Javascript for循环内的异步进程

Asynchronous Process inside a javascript for loop

本问题已经有最佳答案,请猛点这里访问。

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

1
2
3
4
5
6
7
8
var i;
var j = 10;
for (i = 0; i < j; i++) {

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

我正在尝试显示一系列显示数字0到10的警报。问题是,当回调函数被触发时,循环已经经历了几次迭代,并且它显示了更高的i值。关于如何解决这个问题有什么建议吗?


当所有异步操作启动时,for循环立即运行到完成。当它们在将来完成一段时间并调用回调时,循环索引变量i的值将是所有回调的最后一个值。

这是因为在继续循环的下一个迭代之前,for循环不等待异步操作完成,而且异步回调在将来某个时间被调用。因此,循环完成其迭代,然后在这些异步操作完成时调用回调。因此,循环索引是"完成"的,并且位于所有回调的最终值。

要解决这个问题,必须为每个回调单独保存循环索引。在JavaScript中,实现这一点的方法是在函数闭包中捕获它。这可以通过创建一个专门用于此目的的内联函数闭包(下面显示的第一个示例)来完成,也可以创建一个外部函数,将索引传递给它,并让它为您唯一地维护索引(下面显示的第二个示例)。

截至2016年,如果您有一个完全符合规范ES6的javascript实现,您也可以使用let来定义for循环变量,它将为for循环的每个迭代(下面的第三个实现)唯一定义。但是,请注意,这是ES6实现中最新的实现特性,因此您必须确保您的执行环境支持该选项。

使用.foreach()进行迭代,因为它创建了自己的函数闭包

1
2
3
4
5
someArray.forEach(function(item, i) {
    asynchronousProcess(function(item) {
        console.log(i);
    });
});

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

1
2
3
4
5
6
7
8
9
10
11
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返回回调,如下所示:

1
2
3
4
5
6
var j = 10;
for (var i = 0; i < j; i++) {
    asynchronousProcess(i, function(cntr) {
        console.log(cntr);
    });
}

使用ES6 let

如果您有一个完全支持ES6的JavaScript执行环境,那么可以在您的for循环中使用let,如下所示:

1
2
3
4
5
6
const j = 10;
for (let i = 0; i < j; i++) {
    asynchronousProcess(function() {
        console.log(i);
    });
}

在这样的for循环声明中声明的let将为循环的每次调用(这是您想要的)创建i的唯一值。

用承诺和异步/等待进行序列化

如果您的异步函数返回了一个承诺,并且您希望序列化异步操作以逐个运行,而不是并行运行,并且您在支持asyncawait的现代环境中运行,那么您有更多的选项。

1
2
3
4
5
6
7
8
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


Any recommendation on how to fix this?

几个。可以使用bind:

1
2
3
4
5
for (i = 0; i < j; i++) {
    asycronouseProcess(function (i) {
        alert(i);
    }.bind(null, i));
}

或者,如果您的浏览器支持let(它将出现在下一个EcmaScript版本中,但是火狐已经支持了一段时间),您可以:

1
2
3
4
5
6
for (i = 0; i < j; i++) {
    let k = i;
    asycronouseProcess(function() {
        alert(k);
    });
}

或者,您可以手动执行bind的工作(如果浏览器不支持,但我认为您可以在这种情况下实现一个填充程序,它应该在上面的链接中):

1
2
3
4
5
6
7
for (i = 0; i < j; i++) {
    asycronouseProcess(function(i) {
        return function () {
            alert(i)
        }
    }(i));
}

我通常喜欢使用let,当我可以使用它时(例如用于firefox附加组件);否则,bind或自定义的currying函数(不需要上下文对象)。


async await在这里(ES7),所以你现在可以很容易地做这类事情。

1
2
3
4
5
6
  var i;
  var j = 10;
  for (i = 0; i < j; i++) {
    await asycronouseProcess();
    alert(i);
  }

记住,只有当asycronouseProcess返回Promise时,这才有效。

如果asycronouseProcess不在你的控制范围内,那么你可以让它自己返回一个Promise,就像这样

1
2
3
4
5
6
7
function asyncProcess() {
  return new Promise((resolve, reject) => {
    asycronouseProcess(()=>{
      resolve();
    })
  })
}

然后用await asyncProcess();替换这条线await asycronouseProcess();

在调查async await之前,必须先了解Promises。(另请阅读有关支持async await的内容)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();

这里是一个示例函数方法,用于实现这里预期的功能。


ES2017:您可以将异步代码包装在返回承诺的函数(比如xhrpost)中(异步代码在承诺中)。

然后调用for循环内的函数(xhrpost),但使用神奇的wait关键字。:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);
       }
})();


JavaScript代码运行在一个线程上,因此在开始下一个循环之前,您不能主要阻止等待第一个循环迭代完成,而不会严重影响页面可用性。

解决办法取决于你真正需要什么。如果示例与您所需要的非常接近,@simon建议将i传递给异步进程,这是一个很好的例子。