您如何将变异数据携带到循环内的回调中?

How do you carry mutating data into callbacks within loops?

我经常 运行 遇到这种模式在循环内回调的问题:

while(input.notEnd()) {
    input.next();
    checkInput(input, (success) => {
        if (success) {
            console.log(`Input ${input.combo} works!`);
        }
    });
}

这里的目标是检查input的每一个可能的值,并在确认后显示通过异步测试的那些。假设 checkInput 函数执行此测试,返回一个布尔值 pass/fail,并且是外部库的一部分并且无法修改。

假设 input 循环遍历多代码电子珠宝保险箱的所有组合,.next 递增组合,.combo 读出当前组合,checkInput 异步检查组合是否正确。正确的组合是 05-30-96、18-22-09、59-62-53、68-82-01 和 85-55-85。您希望看到的输出是这样的:

Input 05-30-96 works!
Input 18-22-09 works!
Input 59-62-53 works!
Input 68-82-01 works!
Input 85-55-85 works!

相反,因为在回调被调用时,input 已经前进了不确定的次数,并且循环可能已经终止,您可能会看到如下内容:

Input 99-99-99 works!
Input 99-99-99 works!
Input 99-99-99 works!
Input 99-99-99 works!
Input 99-99-99 works!

如果循环已经终止,至少会很明显出了点问题。如果 checkInput 函数特别快,或者循环特别慢,您可能会得到 随机输出 取决于 input 恰好在回调检查它的那一刻。

如果你发现你的输出是完全随机的,这是一个非常难以追踪的错误,对我来说,提示往往是你总是得到预期的 number 输出, 他们只是 错误 .

这通常是在我编写一些复杂的解决方案来尝试保留或传递输入时,如果输入数量很少,这会起作用,但当你有数十亿个输入时,它就不会起作用,其中极少数是成功的(提示,提示,组合锁实际上是一个很好的例子)。

这里是否有一个通用的解决方案,将值传递给回调,就像带有回调的函数第一次计算它们时的值一样?

如果您想一次迭代一个异步操作,则不能使用 while 循环。 Javascript 中的异步操作不是阻塞的。因此,您的 while 循环所做的是 运行 通过对每个值调用 checkInput() 的整个循环,然后在将来的某个时间调用每个回调。他们甚至可能不会按所需的顺序被调用。

因此,根据您希望它的工作方式,这里有两个选项。

首先,您可以使用一种不同类型的循环,它仅在异步操作完成时才进入循环的下一次迭代。

或者,其次,您可以 运行 将它们全部并行处理,就像您正在做的那样,并为每个回调唯一地捕获对象的状态。

我假设您可能想要对异步操作进行排序(第一个选项)。

排序异步操作

你可以这样做(适用于 ES5 或 ES6):

function next() {
    if (input.notEnd()) {
        input.next();
        checkInput(input, success => {
            if (success) {
                // because we are still on the current iteration of the loop
                // the value of input is still valid
                console.log(`Input ${input.combo} works!`);
            }
            // do next iteration
            next();
        });
    }
}
next();

运行并行,在ES6

的本地范围内保存相关属性

如果您想像原始代码那样并行 运行 它们,但仍然能够在回调中引用正确的 input.combo 属性,那么您d 必须将 属性 保存在一个闭包中(上面的第二个选项),这 let 变得相当容易,因为它对于 while 循环的每次迭代都是单独的块范围,因此保留其值当回调 运行s 并且没有被循环的其他迭代覆盖时(需要 ES6 支持 let):

while(input.notEnd()) {
    input.next();
    // let makes a block scoped variable that will be separate for each
    // iteration of the loop
    let combo = input.combo;
    checkInput(input, (success) => {
        if (success) {
            console.log(`Input ${combo} works!`);
        }
    });
}

运行并行,在ES5

的本地范围内保存相关属性

在 ES5 中,您可以引入函数作用域来解决 let 在 ES6 中所做的相同问题(为循环的每次迭代创建一个新作用域):

while(input.notEnd()) {
    input.next();
    // create function scope to save value separately for each
    // iteration of the loop
    (function() {
        var combo = input.combo;
        checkInput(input, (success) => {
            if (success) {
                console.log(`Input ${combo} works!`);
            }
        });
    })();
}

您可以使用新功能 async await 进行异步调用,这样您就可以在循环内等待 checkInput 方法完成。

您可以阅读有关异步等待的更多信息here

我相信下面的代码片段可以实现您的目标,我创建了一个模拟输入行为的 MockInput 函数。注意 doAsyncThing 方法中的 Asyncawait 关键字,并在 运行 时留意控制台。

希望这能澄清事情。

function MockInput() {
  this.currentIndex = 0;
  this.list = ["05-30-96", "18-22-09", "59-62-53", "68-82-0", "85-55-85"];
  
  this.notEnd = function(){
    return this.currentIndex <= 4;
  };
  
  this.next = function(){
      this.currentIndex++;
  };
  
  this.combo = function(){
      return this.list[this.currentIndex];
  }
}

function checkInput(input){
  return new Promise(resolve => {
      setTimeout(()=> {
        var isValid = input.currentIndex % 2 > 0; // 'random' true or false
        resolve( `Input ${input.currentIndex} - ${input.combo()} ${isValid ? 'works!' : 'did not work'}`);
      }, 1000);
  });
}

async function doAsyncThing(){
  var input = new MockInput();
  
  while(input.notEnd()) {
      var result = await checkInput(input);
      console.log(result);
      input.next();
  }
  
  console.log('Done!');
}

doAsyncThing();