我们的 await 关键字在 for await of 循环中的行为与普通异步函数中的行为有何不同?

Posted

技术标签:

【中文标题】我们的 await 关键字在 for await of 循环中的行为与普通异步函数中的行为有何不同?【英文标题】:How differently does our await keyword behave inside the for await of loop than in a normal async function? 【发布时间】:2021-08-30 13:05:38 【问题描述】:

这是一个简单的异步函数:

async function f()

    let data = await (new Promise(r => setTimeout(r, 1000, 200)));
    
    console.log(data)
     
    return data + 100;   


f()

console.log('me first');

以下是该计划中将要发生的事情:

    f() 将被调用 它进入 f 执行上下文并运行 await 表达式 - 由于函数调用的优先级高于 await 关键字,我们的新 Promise() 函数将首先运行,该函数将立即返回一个待处理的 Promise,然后等待关键字将起作用. 我们设置了一个计时器,它将在 1 秒后解决我们的承诺 由于尚未解决,await 将返回一个待处理的 Promise,我们会从当前执行上下文中跳出一层,这恰好是我们开始执行全局代码的全局执行上下文。 我们点击“console.log('me first')”行并记录“me first” 现在 1 秒后,我们的 promise 已经解决,这意味着我们的 await 表达式后面的所有代码都将被放入一个容器中,并放入微任务队列中,以便在下一次执行时执行! (在底层它可能会调用 .next() 方法,就像生成器函数一样,但我们不要深入探讨) 我们的同步代码已完成,我们的微任务队列中有一个任务,我们将其放入调用堆栈以运行! 它运行,一旦我们回到我们的异步函数,它就会从它离开的地方开始,即等待表达式。 由于我们的 promise 现在已解决,我们的 await 关键字将从已解决的 promise 中提取值并将其分配给变量 d。 我们将该值记录到下一行。 最后,在最后一行,我们从 async 函数返回值 + 100,这意味着我们之前返回的待处理承诺(如第 4 行中所述)使用该值解决,即 300!

如您所见,此时我对异步等待非常满意! 现在这是我们等待迭代的代码!

    async function f()

        
        let p1 = new Promise(function (r)
            setTimeout(r, 2000, 1)
        )
        let p2 = new Promise(function (r)
            setTimeout(r, 1000, 2)
        )
        let p3 = new Promise(function (r)
            setTimeout(r, 500, 3)
        )
     
        let aop = [p1, p2, p3];
        
        for await (let elem of aop )
            
            let data = await elem;
            console.log(data)
        
        
        

f()
    
    console.log('global');

现在直观地说,假设 await 的行为与我们之前的 async 函数中的行为相同,您会期望这种情况发生!

    f() 被调用 我们创建了三个 Promise p1、p2、p3 p1 将在 2 秒后解析,p2 在 1 秒后解析,p3 在 500 m/s 后解析 我们将这些 Promise 放在一个名为 aop 的数组中(因为 for 可以使用具有 Symbol.iterator 作为属性的对象,并且所有数组都具有该属性。) 现在这就是我觉得棘手的地方! 我们将遍历每个元素,对于第一个元素,我们进入循环并立即等待我们的第一个元素,即我们的第一个承诺 p1。 由于它还没有解决,它会像我们之前的示例一样返回一个待处理的承诺,或者是吗? 对于其他两个 Promise,p2 和 p3 也会发生同样的情况,返回更多两个待处理的 Promise。 我们的异步函数完成了,我们在全局执行中再往外一层,打印出 global:console.log('global'); 此时,我们的 p3 承诺已经解决,因为它应该只需要 500 毫秒才能完成,所以它首先解决了!接着!也许在那之后我们留下的任何代码(console.log(data))都会被放入微任务队列以供以后执行? 按照这个逻辑,它应该打印 3,因为我们的最后一个承诺首先解决,然后是 2,然后是 1,但它打印 1、2、3! 那么我在写作中遗漏了哪些关键点? 因为 await 显然是并行的,所以它会遍历每个元素而不等待第一个元素完成,那么为什么它会这样工作?

【问题讨论】:

How do I format my posts using Markdown or html? 您说的是“500ms”,但您的代码说的是“5000”毫秒。 @StriplingWarrior - 糟糕,我刚刚编辑了!感谢您指出。 5 年前当我们做出决定时,我在找到一个讨论中说“人们会对这种行为感到惊讶”后,我只是大声笑了起来:D @imheretolearn1 不,[p1, p2, p3] 不是按其解析顺序排列的 promise 流,它是一个数组,将按顺序迭代。 for 循环不会跳过元素或任意重新排序它们 - 并且 for await 循环在前进之前等待每个迭代器步骤。不,这与阻塞代码无关,它仍然是异步的 - 注意 await 关键字。 【参考方案1】:

当你得到一个承诺时,for await 循环有特殊的行为。

记住 - 承诺是价值 - 一旦你有一个承诺,创建该承诺的动作(在这种情况下设置计时器)已经发生。承诺就是该行动的价值+时间。

这里发生的实际上是:

您将您的一系列承诺(只是已开始操作的结果)传递给for...await。 由于该数组没有Symbol.asyncIterator - 循环检查它是否有Symbol.iterator。由于数组是可迭代的并且具有该符号 - 它被调用。 for... await 循环检查每个返回值是否是一个承诺 - 如果是,它会在继续循环之前等待它。

由于这些值是承诺 - 它们将被排序(也就是说,只有在前一个 .next 调用返回的承诺已解决后,才会在从 [p1, p2, p3][Symbol.iterator]() 返回的迭代器上调用 .next()


注意:

您使用的是resolved,但意思是settledfulfilled

【讨论】:

如果有人关心 - 这个特殊的设计选择(等待消费者方面的价值)是一个非常有趣的讨论,我很乐意挖掘它 实际上重新阅读您的问题我认为您感到困惑的行为只是for...await 将等待迭代器传递给它的承诺? 有趣的引述:“我想,如果我们决定采取其他方式,很多人会感到惊讶” - github.com/tc39/proposal-async-iteration/issues/15 还有更多关于这种行为的历史背景:github.com/tc39/proposal-async-iteration/issues/… @imheretolearn1 把它想象成一个普通的for... of 循环,除了它检查它收到的每个值是否是一个承诺,如果是它awaits 它(在消费者方面)【参考方案2】:

我无法谈论每个引擎(甚至是 polyfill)中作业的具体实现,但创建 Promise 不应该将代码推送到单独的堆栈中。 await 机制应该可以做到这一点。

因为我们谈论的是常规迭代。代码应该与以下内容没有太大不同:

console.log(await p1);
console.log(await p2);
console.log(await p3);

精确的语义,当然不完全一样。迭代器仍然保证顺序,这是重点。即使 p3 先解析,在等待 p2p1 之后会在等待之后检索该值。

从技术上讲,每个作为 promise 的值都被 awaited 和 for await(... of ...) 绑定,因此,awaiting 绑定的值实际上并没有什么作用(除了可能将进一步的代码推入下一个堆栈)。

例如你有console.log(await await p1);

【讨论】:

实际上语义非常接近:-)

以上是关于我们的 await 关键字在 for await of 循环中的行为与普通异步函数中的行为有何不同?的主要内容,如果未能解决你的问题,请参考以下文章

我们需要在控制器中使用 async/await 关键字吗?

如何修复“解析错误:保留关键字‘await’”

用 async/await 来处理异步

C# 中的Async 和 Await 的用法详解

C# 中的Async 和 Await 的用法详解

在 then 回调中使用 await - 保留关键字“await”