当 await 表达式是 concat() 的参数时,为什么 async/await 有不同的输出?

Why async/await have different output when await expression is a argument of concat()?

我对下面的示例感到困惑。

我理解为什么 output2[1000,2000,3000] 因为闭包,这就是为什么 map() 中的所有异步函数更新同一个数组 output2。 (如有错误请指正。)

但是,我不明白为什么output1[3000]

我可以知道为什么 run1 不像 run2 那样吗?你能告诉我区别吗?

"use strict";

function sleep(ms) {
  return new Promise(resolve =>
    setTimeout(() => {
      resolve(ms);
    }, ms)
  );
}

const seconds = [1000, 3000, 2000];

let output1 = [];
let output2 = [];

(async function run1() {
  await Promise.all(
    seconds.map(async sec => {
      output1 = output1.concat([await sleep(sec)]);
    })
  );
  console.log({ output1 });
})();

(async function run2() {
  await Promise.all(
    seconds.map(async sec => {
      const res = await sleep(sec);
      output2 = output2.concat([res]);
    })
  );
  console.log({ output2 });
})();

我在第一个示例之间添加了一行。猜猜这将帮助您了解原因。

"use strict";

function sleep(ms) {
  return new Promise(resolve =>
    setTimeout(() => {
      resolve(ms);
    }, ms)
  );
}

const seconds = [1000, 3000, 2000];

let output1 = [];

(async function run1() {
  await Promise.all(
    seconds.map(async sec => {
      const dummy = output1;
      console.log(`dummy_${sec}`, dummy);
      output1 = dummy.concat([await sleep(sec)]);
    })
  );
  console.log({ output1 });
})();


更新:

以下是您使用生成器 polyfill 转译为 ES5 代码的原始代码。注意转译器如何在 output1.concat([ /* pause here */ ]) 调用之间暂停

诀窍是在 yield 之前将 .concat 就地绑定到 output1,然后等待继续。

在这个绑定发生的那一刻,output1 == [],这就是我想通过重新分配给 dummy 变量并在前面的代码中打印它来强调的。

var seconds = [1000, 3000, 2000];
var output1 = [];
(function run1() {
    return __awaiter(this, void 0, void 0, function () {
        var _this = this;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, Promise.all(seconds.map(function (sec) { return __awaiter(_this, void 0, void 0, function () {
                        var _a, _b;
                        return __generator(this, function (_c) {
                            switch (_c.label) {
                                case 0:
                                    _b = (_a = output1).concat;
                                    return [4 /*yield*/, sleep(sec)];
                                case 1:
                                    output1 = _b.apply(_a, [[_c.sent()]]);
                                    return [2 /*return*/];
                            }
                        });
                    }); }))];
                case 1:
                    _a.sent();
                    console.log({ output1: output1 });
                    return [2 /*return*/];
            }
        });
    });
})();

查看声明

 output1 = output1.concat([await sleep(sec)]);

左侧output1是一个变量标识符,用于提供存储右侧计算结果的位置。变量的绑定不会改变,它始终提供变量值的位置。

右侧 output1 是一个值 - 从变量名称提供的位置检索的值。

现在,如果 JavaScript 引擎在继续评估之前检索到 output1 的值,所有三个映射函数调用

  • 检索对存储在 output
  • 中的空数组的引用
  • 等待计时器承诺并将 output1 设置为新值 是 return 从 concat 方法编辑的数组。

因此每个 map 操作将一个包含计时器值的数组连接到一个空数组,并将结果存储在 output1 覆盖之前等待的操作的结果.

这解释了为什么当 Promise.all 结算时您只能看到存储在 output1 中的最后一个数组。我还将收回上面“如果 JavaScript 引擎...”的措辞。 JavaScript 引擎确实在 await:

之前获得了 output1 的值

function sleep(ms) {
  return new Promise(resolve =>
    setTimeout(() => {
    console.log( output1.length);
      resolve(ms);
    }, ms)
  );
}

const seconds = [1000, 3000, 2000];

let output1 = [];
let output2 = [];

(async function run1() {
  await Promise.all(
    seconds.map(async sec => {
      output1 = output1.concat([await sleep(sec)]);
      //output1.push(await sleep(sec));
      console.log(output1[0]);
    })
  );
  console.log({ output1 });
})();

let count = 6;
let timer = setInterval( ()=> {
  console.log(output1[0])
  if(--count <=0 ) {
     clearInterval( timer);
  }
}, 500);


要弄清楚为什么第二种方法 (run2) 有效(这与闭包的存在无关):

.map方法同步调用map函数同步return一个promise无需等待用于计时器承诺履行。

在第二个版本中,

seconds.map(async sec => {
  const res = await sleep(sec);
  output2 = output2.concat([res]);
}

const res = await sleep( sec) 行保存执行上下文并等待 sleep 承诺履行。当承诺完成时,await 恢复保存的上下文并将承诺值存储在 res 中。下一行

  ouput2 = output2.concat([res]);

在 计时器到期后执行 并且在右侧将在执行该行时加载 output2 current 的值,如果由先前的计时器到期更新发生了一个。

将此与 run1 进行对比,其中 JavaScript 引擎在开始计算赋值运算符右侧的表达式时基本上缓存了 ouput1 的值并使用如代码片段中所示,所有迭代的空数组值相同。

*副本的 表示加法运算的左手操作数是在右手操作数被 return 编辑 await 之前从存储中检索的。在 run1 的情况下,我们看到调用方法 的对象(output1 的值)在参数值之前被检索用于调用的方法已经确定。正如对链接答案的评论中所述,这是一个相当“隐藏的陷阱”。