在JavaScript循环中的异步处理

190

我正在运行以下形式的事件循环:

var i;
var j = 10;
for (i = 0; i < j; i++) {

    asynchronousProcess(callbackFunction() {
        alert(i);
    });
}

我正在尝试显示一系列警报,显示数字0到10。问题是,在回调函数触发之前,循环已经进行了几次迭代,���显示了更高的i值。有什么建议可以解决这个问题吗?


将 i 参数添加到 asynchronousProcess 函数中如何?这样可以将其传递给 callbackFunction。 - Simon Forsberg
将“i”传递到callbackFunction中,以确保您处理该变量的值与函数调用时相同 - 而不是在函数返回后。 - Drkawashima
6个回答

323
for循环会立即执行完毕,而所有的异步操作却在稍后开始。当它们完成并调用它们的回调函数时,你的循环索引变量i将对于所有回调函数都保持在其最后一个值。
这是因为for循环在继续下一次迭代之前不会等待异步操作完成,而异步回调会在未来的某个时间被调用。因此,循环完成其迭代,然后异步操作完成时再调用回调函数。因此,循环索引已经“完成”,并且对于所有回调函数都处于其最终值。
要解决这个问题,你必须为每个回调函数单独保存循环索引。在Javascript中,可以通过在函数闭包中捕获索引来实现。这可以通过创建一个专门用于此目的的内联函数闭包(如下面的第一个示例所示)或者创建一个外部函数,将索引传递给它,并让它为你唯一地维护索引(如下面的第二个示例所示)。
截至2016年,如果你有一个完全符合ES6规范的Javascript实现,你也可以使用let来定义for循环变量,它会在每次for循环迭代中被唯一地定义(如下面的第三个实现所示)。但是请注意,这是ES6实现中的一个晚期特性,因此你必须确保你的执行环境支持该选项。 使用.forEach()来迭代,因为它会创建自己的函数闭包
someArray.forEach(function(item, i) {
    asynchronousProcess(function(item) {
        console.log(i);
    });
});

使用IIFE创建您自己的函数闭包

var j = 10;
for (var i = 0; i < j; i++) {
    (function(cntr) {
        // here the value of i was passed into as the argument cntr
        // and will be captured in this function closure so each
        // iteration of the loop can have it's own value
        asynchronousProcess(function() {
            console.log(cntr);
        });
    })(i);
}

创建或修改外部函数并将变量传递给它

如果你可以修改asynchronousProcess()函数,那么你可以直接将值传递给它,并让asynchronousProcess()函数将cntr传回回调函数,代码如下:

var j = 10;
for (var i = 0; i < j; i++) {
    asynchronousProcess(i, function(cntr) {
        console.log(cntr);
    });
}

使用ES6中的let

如果您的Javascript执行环境完全支持ES6,您可以像这样在for循环中使用let:

const j = 10;
for (let i = 0; i < j; i++) {
    asynchronousProcess(function() {
        console.log(i);
    });
}

for循环的声明中使用let,会为每次循环调用创建一个唯一的i值(这正是你想要的)。

使用Promise和async/await进行序列化

如果您的异步函数返回一个Promise,并且您希望将异步操作序列化以依次运行而不是并行运行,并且您正在运行支持asyncawait的现代环境,则有更多选项。

async function someFunction() {
    const j = 10;
    for (let i = 0; i < j; i++) {
        // wait for the promise to resolve before advancing the for loop
        await asynchronousProcess();
        console.log(i);
    }
}
这将确保仅有一个对asynchronousProcess()的调用正在运行,并且for循环在每个调用完成之前都不会继续进行。这与之前并行运行异步操作的方案不同,因此完全取决于您想要哪种设计。请注意:await使用promise,因此您的函数必须返回在异步操作完成时已解决/拒绝的promise。还要注意,为了使用await,包含该语句的函数必须声明为async

同时运行异步操作并使用Promise.all()按顺序收集结果

 function someFunction() {
     let promises = [];
     for (let i = 0; i < 10; i++) {
          promises.push(asynchonousProcessThatReturnsPromise());
     }
     return Promise.all(promises);
 }

 someFunction().then(results => {
     // array of results in order here
     console.log(results);
 }).catch(err => {
     console.log(err);
 });

1
如果您可以修改asycronouseProcess()函数,我们增加了第二个选项。 - jfriend00
1
在异步函数内部递增计数器并检查它是否等于 j,这样做是否有问题? - Jake 1986
1
值得一读的解释 - async/await - Srinivasan K K
1
@SeanMC - 我理解你的意思,但是这个问题实际上并没有展示任何数组,所以似乎这个问题并不是关于使用for/of来迭代一个数组(或者其他可迭代对象)的。 - jfriend00
1
这是我读过的JS异步行为最清晰的例子之一。你有博客吗? - Demian Sims
显示剩余7条评论

32

async await已经在ES7中出现,因此你现在可以非常轻松地实现这种操作。

  var i;
  var j = 10;
  for (i = 0; i < j; i++) {
    await asycronouseProcess();
    alert(i);
  }

请记住,此方法仅在 asycronouseProcess 返回一个 Promise 时才有效

如果 asycronouseProcess 不在您的控制范围内,那么您可以通过以下方式自己使其返回一个 Promise

function asyncProcess() {
  return new Promise((resolve, reject) => {
    asycronouseProcess(()=>{
      resolve();
    })
  })
}

然后将这行代码 await asycronouseProcess(); 替换为 await asyncProcess();

在深入研究 async await 之前,了解 Promises 是必须的 (还要阅读有关对 async await 的支持的内容)


循环的每次迭代都会等待吗? - Shamoon
@Shamoon 是的。它会等待(如果asycronouseProcess()返回一个Promise)。 - Gerard Setho
1
@Praveena。从异步函数返回一个Promise解决了我的问题。谢谢。 - ozzyzig

11

有没有任何建议来修复这个问题?

有几种方法可以解决。你可以使用bind

for (i = 0; i < j; i++) {
    asycronouseProcess(function (i) {
        alert(i);
    }.bind(null, i));
}

或者,如果你的浏览器支持 let (它将会在下一个 ECMAScript 版本中出现,但是 Firefox 已经支持了一段时间),你可以这样写:

for (i = 0; i < j; i++) {
    let k = i;
    asycronouseProcess(function() {
        alert(k);
    });
}

或者,你可以手动执行bind的工作(在浏览器不支持它的情况下,但我会说,在这种情况下,你可以实现一个shim,它应该在上面的链接中):

for (i = 0; i < j; i++) {
    asycronouseProcess(function(i) {
        return function () {
            alert(i)
        }
    }(i));
}

通常我会尽可能使用let(例如对于Firefox插件);否则使用bind或自定义的柯里化函数(不需要上下文对象)。


ECMAScript的例子是展示let的功能非常好的一个。 - hazelnut
1
asyncronouseProcess 在所有答案中都是某种占位符吗?我收到了“未定义”的错误。 - JackHasaKeyboard
1
“asyncronouseProcess”是原问题的一部分,所以如果它给出“未定义”,那么这很正常。如果您想检查原始问题以及建议的解决方案如何工作,可以将其替换为任何异步函数。例如:function asycronouseProcess(fn){ setTimeout(fn, 100);} - ZER0

4

var i = 0;
var length = 10;

function for1() {
  console.log(i);
  for2();
}

function for2() {
  if (i == length) {
    return false;
  }
  setTimeout(function() {
    i++;
    for1();
  }, 500);
}
for1();

这里是一个样例函数式方法,符合本文的预期要求。


4

ES2017:你可以将异步代码包装在一个返回promise的函数中(比如XHRPost)。

然后,在for循环中调用该函数(XHRPost),但使用神奇的await关键字。 :)

let http = new XMLHttpRequest();
let url = 'http://sumersin/forum.social.json';

function XHRpost(i) {
  return new Promise(function(resolve) {
    let params = 'id=nobot&%3Aoperation=social%3AcreateForumPost&subject=Demo' + i + '&message=Here%20is%20the%20Demo&_charset_=UTF-8';
    http.open('POST', url, true);
    http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    http.onreadystatechange = function() {
    console.log("Done " + i + "<<<<>>>>>" + http.readyState);
          if(http.readyState == 4){
              console.log('SUCCESS :',i);
              resolve();
          }
         }
    http.send(params);       
    });
 }
 
(async () => {
    for (let i = 1; i < 5; i++) {
        await XHRpost(i);
       }
})();


2

JavaScript代码在单线程上运行,因此您原则上不能阻止等待第一次循环迭代完成后才开始下一次,否则将严重影响页面的可用性。

解决方案取决于您实际需要的内容。如果示例与您所需的内容非常接近,则@Simon的建议将i传递给异步进程是一个不错的选择。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接