我正在运行以下形式的事件循环:
var i;
var j = 10;
for (i = 0; i < j; i++) {
asynchronousProcess(callbackFunction() {
alert(i);
});
}
我正在尝试显示一系列警告,显示数字0到10.问题是,当回调函数被触发时,循环已经经历了几次迭代,并且它显示更高的值{{ 1}}。关于如何解决这个问题的任何建议?
答案 0 :(得分:164)
for
循环立即运行,同时启动所有异步操作。当他们将来完成一些时间并调用他们的回调时,循环索引变量i
的值将是所有回调的最后一个值。
这是因为for
循环不等待异步操作完成,然后继续循环的下一次迭代,并且因为异步回调在将来的某个时间被调用。因此,循环完成其迭代,然后在异步操作完成时调用回调。因此,循环索引是“完成”并且位于所有回调的最终值。
要解决此问题,必须为每个回调单独保存循环索引。在Javascript中,这样做的方法是在函数闭包中捕获它。这可以通过专门为此目的创建内联函数闭包来完成(第一个示例如下所示),或者您可以创建一个将索引传递给的外部函数,并让它为您唯一地维护索引(下面显示的第二个示例)。
截至2016年,如果您拥有完全符合规范的J6 ES6实现,您还可以使用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
如果您拥有完全支持ES6的Javascript执行环境,则可以在let
循环中使用for
,如下所示:
const j = 10;
for (let i = 0; i < j; i++) {
asynchronousProcess(function() {
console.log(i);
});
}
在这样的let
循环声明中声明的 for
将为每次循环调用创建一个唯一的i
值(这就是你想要的)。
使用promises和async / await进行序列化
如果您的异步函数返回一个承诺,并且您希望将异步操作序列化为一个接一个而不是并行运行,并且您在支持async
和await
的现代环境中运行,那么你有更多的选择。
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
。
答案 1 :(得分:10)
有关如何解决这个问题的任何建议吗?
几个。您可以使用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
的工作(如果浏览器不支持它,但我会说你可以在这种情况下实现垫片,它应该在上面的链接中):
for (i = 0; i < j; i++) {
asycronouseProcess(function(i) {
return function () {
alert(i)
}
}(i));
}
当我可以使用它时,我通常更喜欢let
(例如,对于Firefox附加组件);否则bind
或自定义currying函数(不需要上下文对象)。
答案 2 :(得分:9)
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();
在调查Promises
之前了解async await
必须
(另请阅读有关async await
)
答案 3 :(得分:1)
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();
&#13;
以下是这里所期望的功能方法示例。
答案 4 :(得分:0)
JavaScript代码在单个线程上运行,因此您不能主要阻止等待第一个循环迭代在开始下一个迭代之前完成而不会严重影响页面可用性。
解决方案取决于您真正需要的是什么。如果示例接近您所需要的,@ Simon建议将i
传递给您的异步进程是一个很好的建议。
答案 5 :(得分:0)
ES2017:您可以将异步代码包装在返回诺言的函数中(例如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);
}
})();