171

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

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

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

我试图显示一系列显示数字 0 到 10 的警报。问题是,当回调函数被触发时,循环已经经历了几次迭代,它显示了更高的i. 有关如何解决此问题的任何建议?

4

6 回答 6

292

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);
    });
}

使用 ES6let

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

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

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

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

如果您的 async 函数返回一个 Promise,并且您希望序列化您的异步操作以一个接一个地运行而不是并行运行,并且您在支持asyncand的现代环境中运行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

并行运行异步操作并用于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);
 });
于 2012-07-14T23:18:05.610 回答
24

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

于 2017-06-11T19:21:04.477 回答
11

关于如何解决这个问题的任何建议?

一些。您可以使用绑定

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或自定义柯里化函数(不需要上下文对象)。

于 2012-07-14T23:59:46.183 回答
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();

这是此处预期的示例功能方法。

于 2017-08-01T07:33:29.050 回答
4

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);
       }
})();

于 2018-08-18T01:29:50.880 回答
2

JavaScript 代码在单个线程上运行,因此您不能在不严重影响页面可用性的情况下阻塞等待第一个循环迭代完成后再开始下一个循环。

解决方案取决于您真正需要什么。如果该示例完全符合您的需要,@Simon 建议传递i给您的异步进程是一个很好的建议。

于 2012-07-14T23:11:02.873 回答