【问题标题】:Working of call stack when async/await is used使用 async/await 时调用堆栈的工作
【发布时间】:2019-10-08 00:33:44
【问题描述】:

使用 async/await 函数时调用堆栈的行为如何?


function resolveAfter2Seconds() { // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

const asyncFuntion=async()=>{
  const result = await resolveAfter2Seconds();
  console.info("asyncFuntion finish, result is: ", result);
}

const first = async()=>{
    await asyncFuntion();
    console.log('first completed');
        debugger;
}

const second = ()=>{
    console.log('second completed');
        debugger;
}

function main(){
    first();
    second();
}


main();

在上面的代码中,当在 second() 中遇到第一个断点时,我可以看到调用堆栈包含 main() 和 second()。在 first() 中的第二个断点期间,调用堆栈包含 main() 和 first()。

在第一个断点期间 first() 发生了什么。推到哪里去了?假设 asyncFunction() 需要一些时间才能完成。

请有人帮忙。

【问题讨论】:

  • synchronousasynchronous 函数混合在一起时会出现问题。我不太确定强硬,但我可以想象,main() 开始,并将first() 推到调用堆栈x,因为它是async。在调用first() 之后,立即调用second() 并将其推送到不同的调用堆栈,因为sync。我通常把async 想象成某种线程。
  • 看来你的回答是合乎逻辑的。但是,这与 JS 引擎只有一个调用堆栈这一事实不矛盾吗?
  • 是的,nodes 有一个 eventloop。但eventloop 只是最后的刽子手。我认为仍然会有不同的callstacks 或类似队列的东西。最后,事件循环获取所有这些队列并以某种顺序执行它们。我没有足够的知识来正确回答这个问题,所以我只是把它作为评论。
  • 是的。我知道事件循环从队列中获取回调并将其推入堆栈。但是,由于使用 await 会阻塞 first() 的执行,所以该函数应该存在于调用堆栈本身中,对吧?
  • 我认为,你不能称之为callstack。我认为这只是堆栈跟踪。我想这是处理得更底层。`

标签: javascript ecmascript-6 promise event-loop


【解决方案1】:

首先,当您到达您在second 中命中的断点时,first 已经执行并且不再在堆栈中。

当我们进入first 时,我们立即点击了await asyncFunction()。这告诉 JavaScript 不要阻塞调用的结果,但在我们等待的时候可以随意寻找其他事情要做。 Javascript 是做什么的?

嗯,首先,它确实调用了asyncFunction()。这将返回一个承诺,并将启动一些稍后将解决它的异步过程。现在我们不能继续下一行first(即console.log('first completed')),因为我们的await意味着在promise实现之前我们不能继续,所以我们需要在这里暂停执行,然后去找与我们的空闲时间有关的其他事情。

所以,我们查找堆栈。我们仍然在来自 main 的 first() 调用中,现在我们可以从该调用中返回一个 Promise,我们将在异步执行完成时解决它。 main 忽略该承诺返回值,因此我们继续使用 second()。执行完second 后,我们会回顾调用它的任何内容,并以所有人期望的方式继续同步执行。

然后,在未来的某个时候,我们的承诺会实现。也许它正在等待 API 调用返回。也许它正在等待数据库回复它。在我们的示例中,它正在等待 2 秒超时。无论它在等待什么,现在都可以处理了,我们可以取消暂停 first 调用并继续在那里执行。它不是从main“调用”的——在内部函数被拾取,就像回调一样,但关键是用一个全新的堆栈调用,一旦我们完成调用函数的其余部分,它将被销毁。

鉴于我们在一个新堆栈中,并且早就离开了“主”堆栈框架,当我们在其中遇到断点时,mainfirst 如何再次进入堆栈?

长期以来,如果您在调试器中运行代码,简单的答案是它们不会。你只要得到你所在的函数,调试器就会告诉你它是从“异步代码”或类似的东西中调用的。

然而,现在一些调试器可以跟随等待的代码回到它正在解决的承诺(请记住,awaitasync大部分只是承诺之上的语法糖)。换句话说,当您等待的代码完成并且引擎盖下的“承诺”得到解决时,您的调试器有助于确定堆栈“应该”是什么样子。它显示的内容实际上与引擎最终调用该函数的方式并无太大相似之处——毕竟,它是在事件循环之外调用的。但是,我认为这是一个有用的补充,使我们所有人都能保持代码的思维模型比实际发生的要简单得多!

一些关于它是如何工作的进一步阅读,其中涵盖的细节比我在这里要多得多:

【讨论】:

  • 感谢您的回复。我还有一些问题。我知道 JS 不会等待 first() 完成,而是启动 asyncFunction(),然后将 first() 从堆栈中弹出,然后转到 second()。但是,谁负责 first() 并且它是剩余的执行?是否为它创建了一个新堆栈?还是使用了回调?如果有,怎么做?
  • @ZeeshanShamsuddeen - 更新了答案,希望更清楚。如果您正在寻找大量关于此的详细信息,我建议您查看 JavaScript 事件循环如何工作的详细信息 - 这里已经有很多很好的答案,我不想太接近重复它们.
  • 感谢@Hecksa。但是,我认为事件循环只检查回调队列、微任务队列等是否有任何准备好推送到调用堆栈的任务。你是说回调队列吗?
  • 这个相当复杂的话题的答案非常好。您应该将“信号已发送”替换为“执行已完成”。信号用于此 ping 进程的终止和填充。执行完成不代表成功获取结果。
  • 完成。我希望它仍然合理简洁:-)
猜你喜欢
  • 2012-12-02
  • 1970-01-01
  • 1970-01-01
  • 2013-04-29
  • 2019-08-05
  • 1970-01-01
  • 2019-12-04
  • 1970-01-01
  • 2021-11-18
相关资源
最近更新 更多