【问题标题】:What's the control flow of "for await of"?“for await of”的控制流程是什么?
【发布时间】:2021-08-29 19:32:08
【问题描述】:

(英语不是我的母语,所以如果你发现任何英语,请提前原谅我)

在这一点上,我对 Promises 和 Async await 函数非常满意,我也知道如何使用 Promise.all(它只是等待里面的所有 Promise 先解决,然后从所有 Promise 中提取值,然后使用带有这些值的 .then 函数返回一个数组。)

现在我想知道 for await of work 是怎么做的。

我刚刚写了这段代码:

async function f() {

  let p1 = new Promise(function (r) {
    setTimeout(r, 3000, 1)
  })

  let p2 = new Promise(function (r) {
    setTimeout(r, 2000, 2)
  })

  let p3 = new Promise(function (r) {
    setTimeout(r, 1000, 3)
  })

  let arrayOfPromises = [p1, p2, p3];

  for await (let p of arrayOfPromises) {
    let data = await p;
    console.log(data)
  }
}

f();

现在我的问题是,当它到达第一次迭代时会发生什么,它将到达一个 await 关键字,并且 await 立即返回一个待处理的承诺,那么下面的代码在技术上是否会在每次迭代中被评估为待处理的承诺?

  {
    let data = await p;
    console.log(data)
  }

所以我很困惑到底发生了什么,对于第一次迭代,setTimeout 将注册 3 秒,第二次注册 2,第一次注册 1。由于我们没有同步代码,所有的回调都会一个一个地运行,p3会先被解析,然后是p2,最后是p1!

现在直觉上我会认为一旦 p1、p2、p3 被解析,这段代码“console.log(data)”将被放入微任务队列,因为我们的 p3 先被解析,我们应该得到 3,2,1但是我们得到 1、2、3,那么我的理解中缺少什么?

(显然代码没有被放入微任务队列,它的函数可能会像生成器函数一样执行类似 .next() 的操作,但我认为这无关紧要)

似乎与 for await of 相比,第一个 Promise 将首先被记录,无论它与迭代中的其他 Promise 相比解决得有多快或多晚,那么到底发生了什么?

【问题讨论】:

  • await 不返回承诺。它等待承诺被解决。
  • 即使 promise 可能被解决,日志不会发生,直到你的 for 循环评估 promise。所以你的第一个承诺等待三秒钟,然后解决你的第一个循环。这时候,其他人也都解决了。尝试使您的第一个解析为 500 和第二个解析为 1000,您将看到它们之间的 500 毫秒延迟。简而言之,它按顺序检查它们。解决的时间不完全是 for 循环问题。
  • await p 是多余的。在循环同步迭代器时,for await 在分配给 p 之前会自动等待。
  • @Barmar - 嘿,但只要首先遇到等待,异步函数就会返回默认承诺,不是吗?就像在这段代码中一样: async function f(){ let d = await something(); // 此时,一个待处理的 Promise 已经返回 // 我们剩下的任何代码都将在我们等待的 Promise 解决后运行 } 这不正确吗?

标签: javascript for-loop asynchronous async-await promise


【解决方案1】:

这是一个可以按照您的预期工作的代码:

async function printWhenResolved(p) { 
  const data = await p; console.log(data)
}

function createIteratorsAndBindPrinting() {   

  let p1 = new Promise(function (r) {
    setTimeout(r, 3000, 1)
  })

  let p2 = new Promise(function (r) {
    setTimeout(r, 2000, 2)
  })

  let p3 = new Promise(function (r) {
    setTimeout(r, 1000, 3)
  })

  let arrayOfPromises = [p1, p2, p3];

  for (let p of arrayOfPromises) {
    printWhenResolved(p)
  } 
  console.log('end of createIteratorsAndBindPrinting')
}
createIteratorsAndBindPrinting()

现在,从语义上讲,我认为处理看起来很清楚。在“for await (... of ...)”上,迭代一个可迭代对象,在每个点根据迭代的当前值是同步还是异步提供其值来做出决定。如果它同步提供它的值,我们立即执行“for”中的代码块。如果它异步提供它的值,我们等到提供了值,然后调用“for”中的块。

所以我们依次等待每个可迭代对象 - 然后才离开 for-block。

在我提供的代码中,我们改为在每个 Promise 上调用一个异步函数,当它解析后,会将值记录到控制台。这也是为什么您的 let data = await p 是多余的(因为您的 await 也在 for-construct 中)。

通过这种方式,async 函数是一种无需回调地狱或 then-chaining 即可组织响应式代码(即调用过程并具有例如承诺的解析的代码)的方法。这可以说允许更多的模块化、表达性和可测试性。另见https://stackoverflow.com/a/54497100/16005185

要查看与您的代码具体做什么的区别(没有冗余),请将上面的代码与以下代码进行比较:

async function createIteratorsAwaitAndPrint() {   

  let p1 = new Promise(function (r) {
    setTimeout(r, 3000, 1)
  })

  let p2 = new Promise(function (r) {
    setTimeout(r, 2000, 2)
  })

  let p3 = new Promise(function (r) {
    setTimeout(r, 1000, 3)
  })

  let arrayOfPromises = [p1, p2, p3];

  for await (let p of arrayOfPromises) {
    console.log(p)
  } 
  console.log('end of createIteratorsAwaitAndPrint')
}
createIteratorsAwaitAndPrint()

在这里,我们在 for 构造中执行 await。注意“end of [...]”日志在我们完成迭代之前不会发生。

【讨论】:

  • 其实你也不应该这样做——他们没有做正确的错误处理。
  • 这种形式通常不是最佳实践——我只是想让它接近问题中的代码,以证明只需要最少重新思考的东西的语义差异。当然,如果您不只是展示一个孤立的想法 - 您应该始终明确地处理失败。
【解决方案2】:

这正是人们在考虑使用 async await 抽象之前必须远离或至少三思而后行的原因,因为它是对另一个抽象的抽象。

您应该做的只是停留在同步时间线中,并在您的 Promise 中附加一个.then(v => console.log(v)) 阶段,以便按照您期望的顺序打印出他们的结果。

喜欢;

var ps = Promise.all([p1,p2,p3].map(p => p.then(v => (console.log(v), v))));

async 函数中使用for await 循环也没有多大意义。 这里不需要异步函数但即使你使用了一个那么您应该返回arrayOfPromises 数组以在同步代码中对它的值进行循环。 for await 循环在同步代码中很好用。 最近的 JS 引擎允许您使用 for await 循环,而无需在模块中声明异步函数。它需要一个异步迭代(最好但也同步)并将异步迭代中的.next()解析承诺自动转换为要在循环中处理的裸分辨率值。所以它会等到第一个数组项解决后再继续下一个,无论以下承诺是否已经解决。

现在您可以在代码的同步部分执行此操作;

for await (let v of ps){
  console.log(v);
}

它将按照数组中出现的顺序记录值。

【讨论】:

  • "async 函数中使用for await 循环没有多大意义" - 呃,for await 循环只能 在async 函数中使用?不能在同步代码中使用该语法,因为它需要暂停执行 - 就像 await(但多次)。
  • @Bergi 你是对的,但现在有点对了。这正是我说人们应该远离异步等待抽象的原因,因为现在有top level awaitthis question。那么是的,它仍然不是同步代码,但非常相似。不过我喜欢 Deno。
  • @Redu 顶级 await 在异步模块中运行。这并没有真正减损Bergi所说的。要点是 for await 仅在异步上下文中很重要(如果我们必须在这里更加正式)。您的回答说您不应该在异步函数中使用它(这是一种异步上下文)。我认为建议不要在任何异步上下文中使用for await。这……本身并没有多大意义。或者您是否建议在顶级异步模块中使用 for await 而不是在异步函数中?这也没有多大意义。
  • @VLAZ 是的,您已正确阅读。我的句子不正确。但是什么是异步模块?据我了解,所有模块现在都具有顶级等待功能我错了吗?此外,大部分代码现在都驻留在模块中。
  • 顶级await 使模块异步。不过,它是透明的——如果在“foo”模块中执行 import { x } from "bar" 并且 bar 模块在顶层使用 await,这意味着导入将在 bar 模块完成等待承诺。这反过来又使 foo 模块隐式等待。顶级await 使模块表现得像异步函数——一旦你使用await 一次,链中的任何其他东西也必须是异步的才能处理它。但是,顶级await 无需您明确说明每个模块都是异步的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-08-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多