当循环在异步函数内部而不是相反时,为啥 Async/Await 可以正常工作?

Posted

技术标签:

【中文标题】当循环在异步函数内部而不是相反时,为啥 Async/Await 可以正常工作?【英文标题】:Why does Async/Await work properly when the loop is inside the async function and not the other way around?当循环在异步函数内部而不是相反时,为什么 Async/Await 可以正常工作? 【发布时间】:2019-08-14 20:38:24 【问题描述】:

awaitpromise上时,我有三个循环三遍的sn-ps。

在第一个 sn-p 中,它按我的预期工作,i 的值随着每个await 而递减。

let i = 3;

(async () => 
  while (i) 
    await Promise.resolve();
    console.log(i);
    i--;
  
)();

输出:

3
2
1

在第二个中,i的值不断递减直到为零,然后所有awaits都被执行。

let i = 3;

while (i) 
  (async () => 
    await Promise.resolve();
    console.log(i);
  )();
  i--;

输出:

0
0
0

最后,这会导致Allocation failed - javascript heap out of memory 错误并且不打印任何值。

let i = 3;
while (i) 
  (async () => 
    await Promise.resolve();
    console.log(i);
    i--;
  )();


谁能解释他们为什么表现出这些不同的行为?谢谢。

【问题讨论】:

你知道async functions 是如何脱糖的吗? @Bergi 这只是处理承诺的语法糖,而不是使用.then().catch() 方法。我说的对吗? 是的,基本上。那么如果使用then() 编写,您认为您的代码会是什么样子? 对于第二个 sn-p,它将是:while (i) Promise.resolve().then(res => console.log(i)); i--; 没错。现在看来很明显why this logs 0 three times。 【参考方案1】:

关于你的第二个 sn-p:

在不等待结果的情况下调用异步函数称为即发即弃。您告诉 JavaScript 它应该开始一些异步处理,但您并不关心它何时以及如何完成。这就是发生的事情。它循环,触发一些异步任务,当循环完成时,它们会在某个时候完成,并且在循环已经结束时将记录 0。如果你愿意:

await (async () => 
  await Promise.resolve();
  console.log(i);
)();

它会按顺序循环。

关于你的第三个 sn-p:

你永远不会在循环中减少i,因此循环会永远运行。如果异步任务在某个时候执行,它将递减 i,但这不会发生,因为 while 循环疯狂运行并阻塞和崩溃浏览器。

 let i = 3;
 while(i > 0) 
   doStuff();
 

【讨论】:

【参考方案2】:

在您的特定示例中,它递减 i,然后运行 ​​async 代码,如:

let i = 3;

while (i) 
  i--; // <---------------------
  (async () =>             // |
    await Promise.resolve();// |
    console.log(i);         // |
  )();                     // |
 // >---------------------------

关于你的第三个 sn-p,它永远不会减少 i 值,因此循环会永远运行,从而导致应用程序崩溃:

let i = 3;
while (i) 
  (async () => 
    await Promise.resolve(); // await and resolve >-----------
    // the following code doesn't run after it resolves   // |
    console.log(i);                                       // |
    i--;                                                  // |
  )();                                                   // |
  // out from the (async() => )() <-------------------------

【讨论】:

并非如此,如果您删除 await Promise.resolve 它将正确记录。 @Bhojendra Rauniyar 你能检查一下这第三个sn-p吗? @JonasWilms 你的意思是说,如果我们有一个async 函数,而它的主体中没有await,那么它将像任何其他函数一样阻塞并同步运行? the asynchronous code runs in background 这是不正确的。异步代码被延迟,并且由于事件循环正忙于无限循环,异步代码永远不会运行。如果它在后台,输出数字将是随机的,但 i 最终将等于零。 @BhojendraRauniyar JavaScript 是单线程的,所以没有“背景”。一次只能运行一段代码。我在这里模糊了一些细节,但是有一个事件循环可以将挂起的回调出列并运行它直到完成。为了防止阻塞(例如等待计时器或网络请求),代码被分解成更小的回调,以便让竞争事件有机会在单线程中运行。 await 只是“在我们等待的东西可用后安排回调运行”的语法糖。【参考方案3】:

因为在第一种情况下,console.log 和 decrement 彼此同步工作,因为它们都在同一个异步函数中。 在第二种情况下,console.log 异步工作,递减同步工作。 所以先递减,异步函数等待同步函数执行完毕,再用i == 0执行

在第三种情况下,循环体同步执行,并在每次迭代时运行异步函数。因此,减量直到循环结束才能起作用,因此循环中的条件始终为真。直到堆栈或内存已满

【讨论】:

【参考方案4】:

主要关注最后一个例子:

let i = 3;
while (i) 
  (async () => 
    await Promise.resolve();
    console.log(i);
    i--;
  )();

如果我们在不使用 async/await 的情况下重写代码以揭示它真正在做什么,这可能会有所帮助。在后台,异步函数的代码执行被推迟到以后:

let callbacks = [];

let i = 0;
while (i > 0) 
  callbacks.push(() => 
    console.log(i);
    i--;
  );


callbacks.forEach(cb => 
  cb();
);

如您所见,在循环完成之前不会执行任何回调。由于循环永远不会停止,最终 vm 将耗尽存储回调的空间。

【讨论】:

以上是关于当循环在异步函数内部而不是相反时,为啥 Async/Await 可以正常工作?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 async/await Axios 请求不会返回 Data 而不是 Promise 对象 [重复]

当for内部有查询时如何返回异步函数的值

对线程安全, 可重入函数, 异步安全的理解

异步自动释放池

async await 的用法

async函数