执行在调用者中继续,直到调用栈变空。然后 JavaScript 引擎开始运行微任务:它运行先前调度的 PromiseResolveThenableJob,它会调度新的 PromiseReactionJob,并将 promise 链接到传给 await 的值。然后,引擎返回去处理微任务队列,因为在继续主事件循环之前必须清空微任务队列。
对于每个 await,引擎都必须创建两个额外的 promise,并需要至少三个微任务。谁会想到一个 await 表达式会导致这么多的开销?!
我们来看看这些开销来自哪里。第一行负责创建包装器 promise,第二行立即用 await 值 v 完成了包装器 promise。这两行负责一个额外的 promise 加上三个微任务中的两个。如果 v 是一个 promise(这是很常见的情况,因为应用程序通常会 await promise),开销就非常大。比如,开发人员在 42 上 await,那么引擎仍然需要将它包装成一个 promise。
事实证明,规范中已经有一个 promiseResolve 操作,它只在必要时执行包装:
将 Node.js 10 中的 await 与可能在 Node.js 12 中发布的优化版 await 进行比较,显示了这个变更对性能的影响:
async/await 现在优于手写的 promise 代码。这里的关键点是我们通过修改规范显著减少了异步函数的开销——不仅在 V8 中,而且是在所有的 JavaScript 引擎中。
改进的开发者体验
除了性能之外,JavaScript 开发者还关心诊断和修复 bug 方面的问题,这些在处理异步代码时并不总是那么容易。Chrome DevTools 支持异步堆栈跟踪,堆栈跟踪不仅包括堆栈的当前同步部分,还包括异步部分:
在进行本地开发时,这是非常有用的一项功能,但对于已经部署的应用程序,这种方法并不会真正帮助到你。在进行事后调试时,你只会在日志文件中看到 Error#stack 输出,它们并不会告诉你有关异步部分的任何信息。
我们最近一直在研究零成本的异步堆栈跟踪,通过异步函数调用丰富了 Error#stack 属性。”零成本”听起来很令人兴奋,不是吗?Chrome DevTools 会带来重大的开销,那它又是如何做到零成本的?可以看看这个示例,其中 foo 异步调用 bar,bar 在 await promise 时抛出异常:
async function foo() { await bar(); return 42;}async function bar() { await Promise.resolve(); throw new Error('BEEP BEEP');}foo().catch(error => console.log(error.stack));
在 Node.js 8 或 Node.js 10 中运行这段代码将产生以下输出:
$ node index.jsError: BEEP BEEP at bar (index.js:8:9) at process._tickCallback (internal/process/next_tick.js:68:7) at Function.Module.runMain (internal/modules/cjs/loader.js:745:11) at startup (internal/bootstrap/node.js:266:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3)
请注意,虽然对 foo() 的调用会导致错误,但 foo 不是堆栈跟踪的一部分。这让 JavaScript 开发人员在进行事后调试时感到很为难,无论你的代码是部署在 Web 应用程序中还是部署在云容器内。
有趣的是,当 bar 完成时,引擎知道该从哪里继续:在函数 foo 的 await 之后。巧合的是,这也是函数 foo 被暂停的位置。引擎可以使用这些信息来重建部分异步堆栈跟踪,于是输出变为:
$ node --async-stack-traces index.jsError: BEEP BEEP at bar (index.js:8:9) at process._tickCallback (internal/process/next_tick.js:68:7) at Function.Module.runMain (internal/modules/cjs/loader.js:745:11) at startup (internal/bootstrap/node.js:266:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3) at async foo (index.js:2:3)
在堆栈跟踪中,最顶层的函数首先出现,然后是同步堆栈跟踪的其余部分,再然后是函数 foo 中对 bar 的异步调用。在 V8 中,这个变更是通过 --async-stack-traces 标志实现的。
但是,如果将它与 Chrome DevTools 中的异步堆栈跟踪进行比较,你会注意到,堆栈跟踪的异步部分中缺少 foo 的实际调用信息。如前所述,这种方法利用了以下事实:对于 await 来说,恢复和暂停位置是相同的——但对于常规的 Promise#then() 或 Promise#catch() 调用,情况并非如此。
结 论
我们进行了两个重要的优化,让异步函数变得更快:
移除两个额外的 microtick;移除 throwaway promise。最重要的是,我们通过零成本的异步堆栈跟踪改进了开发者体验。
我们还为 JavaScript 开发人员提供了一些很好的性能改进建议:
使用异步函数和 await 代替手写的 promise 代码;坚持使用 JavaScript 引擎提供的原生 promise 实现,这样可以避免为 await 使用两个 microtick。英文原文
https://v8.dev/blog/fast-async
花粉社群VIP加油站
猜你喜欢