实现 Async/Await
深入研究异步回调
承诺拯救
结论
在我的上一篇文章中,我们了解了 JS/TS 中生成器背后的理论。在本文中,我将应用这些概念,并展示如何使用生成器构建类似于async/await 的东西。事实上,async/await就是使用生成器和 Promise 实现的。
深入研究异步回调
首先,我们将通过编写带有回调的示例来展示如何使用生成器处理异步性。
思路如下。使用回调时,我们传递一个函数,该函数将在异步操作完成时被调用。那么,如果我们不调用回调,而是调用next
某个生成器呢?更好的是,如果这个生成器就是调用异步函数的代码呢?这样,我们就可以有一段代码调用某个异步过程,在异步过程未完成时保持暂停,并在异步过程准备就绪时返回执行。看看这个:
如果您不知道...args
上面的实现是什么,请查看扩展语法。
我们用包装我们的异步操作asyncWrapper
。这个包装器只传递一个回调,以便在异步过程完成时将控制权交还给生成器main
。请注意,我们在主代码中看起来完全同步。事实上,仅仅看一下main
,我们无法断言是否有任何异步的东西,尽管给出了提示。还要注意,即使我们没有使用,yield
我们的代码也与使用的代码非常相似。这是因为我们正在从使用代码中抽象出异步部分。async/await
Promises
main
使用像上面那样的回调是可以的,但也存在一些问题。
- 这段代码感觉很奇怪。为什么要
main
知道asyncWrapper
?main
应该可以直接调用异步操作,所有事情都应该在后台处理。 - 我们应该在哪里进行错误处理?
- 如果异步操作多次调用回调怎么办?
- 如果我们想并行运行多个异步操作怎么办?由于 yield 对应于执行中的暂停,我们需要添加一些复杂的代码来判断调用
next
它是为了执行另一个操作,还是因为异步操作已经完成? - 我们遇到了正常回调所存在的问题(回调地狱等)。
承诺拯救
我们可以利用Promises解决上述问题。我们将从一个只有一个 yield 且没有错误处理的简单实现开始,然后再对其进行扩展。
首先,我们需要让我们的异步操作addAsync
返回一个承诺,稍后我们再处理不返回的情况。
为了解决第一个问题,我们需要修改包装器,使其接收我们想要执行的代码,并使其成为一个runner。这样,我们的runner就能完成它需要做的事情,并在代码准备就绪时将控制权交还给我们,同时隐藏所有代码的运行方式。runner需要做两件事:
- 初始化我们的代码。
- 接受对其做出的承诺,等待其实现,然后将控制权连同已解决的值交还给我们的代码。
就是这样!只要我们使用 Promise,列表中的第 3 个问题就会自动解决。完整代码如下:
让我们来看看执行过程。
- 首先,我们用函数生成器调用我们的运行器
main
。 - 运行器初始化我们的生成器,然后调用
it.next()
。这将控制权交给main
。 - Main 函数一直执行到
yield
。它会产生 的返回值addAsync
,这是一个 Promise。目前,该 Promise 尚未实现。 - 现在控制权掌握在runner手中。它从 generator yield 中解开值并获取 Promise。它添加了一个
.then
,将已完成的 Promise 的值传递给main
。 - 每当承诺得到解决并且运行器将控制权交给时
main
,yield 表达式就会计算承诺的已解决值 (5),并继续执行直到结束。
处理非 Promises 值
目前,我们的runner期望收到一个Promise。然而,根据规范,你可以 await 任何值,无论是否是Promise。幸运的是,解决这个问题非常简单。
考虑以下同步添加函数:
这段代码导致生成器崩溃,因为我们的生成器试图调用 a.then
来获取生成的值。我们可以通过使用 来解决这个问题Promise.resolve
。Promise.resolve(arg)
如果 arg 是Promise,则复制它;否则,将 arg 包装在Promise中。因此,我们的运行器变成了:
现在我们的代码不会因非 Promise 值而崩溃:
如果我们用运行我们的代码addAsync
,我们将获得与以前相同的行为!
处理错误
由于我们使用了Promises,因此我们可以轻松获取异步操作中发生的任何错误/拒绝。每当发生 Promise 拒绝时,我们的运行器只需解开拒绝原因并将其传递给生成器即可进行处理。我们可以使用以下方法来实现.throw
:
现在,我们不仅添加了一个.then
,还.catch
向 yielded 的Promise添加了,如果发生拒绝,我们会将原因抛出到main
。请注意,这也处理了我们正在执行同步操作并且存在正常 的情况throw
。由于我们的运行器位于main
执行堆栈的下方,因此此错误将首先冒泡到 in ,yield
并main
在 中处理try...catch
。如果没有try...catch
,那么它就会冒泡到运行器,由于我们的运行器没有任何,它会再次冒泡,与async/awaittry...catch
中相同。
处理多个收益
我们已经取得了很大进展。现在我们的代码能够处理一个。由于我们使用了Promises,yield
我们的代码已经能够运行多个并行异步操作,因此其他方法都是免费的。然而,我们的运行器无法运行多个语句。以下面的生成器为例:Promise.all
yield
我们的runner可以很好地处理第一个语句yield
,但是它无法正确地将控制权交还给main
第二个语句yield
,超时会结束,什么也不会发生。我们需要为runner添加一些迭代功能,以便能够正确处理多个yield
语句。请看以下代码:
我们使用带有IIFE.next
的递归来迭代生成器。我们不是直接调用,而是使用 Promise 的解包值递归调用此IIFEyield
。该函数首先会将控制权交还给带有解包值的生成器。如果出现另一个 ,则循环重复。请注意,在最后一个yield
(或者如果没有 ),生成器将结束并将控制权交还给运行器。运行器会检查生成器是否已结束,如果结束,则结束执行。
然而有一个问题:如果其中一个 Promise 被拒绝,那么循环就会被打破,我们的 runner 就无法正常运行。为了解决这个问题,我们需要添加一个错误标志,并根据这个标志调用.next
或:.throw
结论
我们实现了非常接近async/await 的功能。如果你看过V8 博客,就会发现我们的程序本质上做了同样的事情。我建议你读一下上面的博文,里面有一个很酷的优化,如果你使用 await 来处理Promise,那么引擎会非常优化,你的代码会比仅仅使用带有 的 Promise 运行得更快.then
。
写到这篇文章,我算是完成了关于生成器的写作,至少目前是这样。还有一个有趣的话题我还没涉及,那就是协程。如果你想了解它,我推荐这篇文章。
我的下一篇文章会讨论一下Symbol或 Myers diff 算法(git 的默认 diff 算法)。如果您有任何疑问、建议或其他意见,欢迎在下方留言!下次再见 :)
鏂囩珷鏉ユ簮锛�https://dev.to/gsarciotto/implementing-async-await-55f