实现 Async/Await 深入研究回调的异步 Promises 来拯救 结论

2025-06-08

实现 Async/Await

深入研究异步回调

承诺拯救

结论

在我的上一篇文章中,我们了解了 JS/TS 中生成器背后的理论。在本文中,我将应用这些概念,并展示如何使用生成器构建类似于async/await 的东西。事实上,async/await就是使用生成器和 Promise 实现的。

深入研究异步回调

首先,我们将通过编写带有回调的示例来展示如何使用生成器处理异步性。

思路如下。使用回调时,我们传递一个函数,该函数将在异步操作完成时被调用。那么,如果我们不调用回调,而是调用next某个生成器呢?更好的是,如果这个生成器就是调用异步函数的代码呢?这样,我们就可以有一段代码调用某个异步过程,在异步过程未完成时保持暂停,并在异步过程准备就绪时返回执行。看看这个:

使用回调实现

如果您不知道...args上面的实现是什么,请查看扩展语法
我们用包装我们的异步操作asyncWrapper。这个包装器只传递一个回调,以便在异步过程完成时将控制权交还给生成器main。请注意,我们在主代码中看起来完全同步。事实上,仅仅看一下main,我们无法断言是否有任何异步的东西,尽管给出了提示。还要注意,即使我们没有使用,yield我们的代码也与使用的代码非常相似。这是因为我们正在从使用代码中抽象出异步部分async/awaitPromisesmain

使用像上面那样的回调是可以的,但也存在一些问题。

  1. 这段代码感觉很奇怪。为什么要main知道asyncWrappermain应该可以直接调用异步操作,所有事情都应该在后台处理。
  2. 我们应该在哪里进行错误处理?
  3. 如果异步操作多次调用回调怎么办?
  4. 如果我们想并行运行多个异步操作怎么办?由于 yield 对应于执行中的暂停,我们需要添加一些复杂的代码来判断调用next它是为了执行另一个操作,还是因为异步操作已经完成?
  5. 我们遇到了正常回调所存在的问题(回调地狱等)。

承诺拯救

我们可以利用Promises解决上述问题。我们将从一个只有一个 yield 且没有错误处理的简单实现开始,然后再对其进行扩展。

首先,我们需要让我们的异步操作addAsync返回一个承诺,稍后我们再处理不返回的情况。

带有 Promises 的 addAsync

为了解决第一个问题,我们需要修改包装器,使其接收我们想要执行的代码,并使其成为一个runner。这样,我们的runner就能完成它需要做事情,并在代码准备就绪时将控制权交还给我们,同时隐藏所有代码的运行方式。runner需要做两件事:

  1. 初始化我们的代码。
  2. 接受对其做出的承诺,等待其实现,然后将控制权连同已解决的值交还给我们的代码。

基本运行器实现

就是这样!只要我们使用 Promise,列表中的第 3 个问题就会自动解决。完整代码如下:

承诺的基本实现

让我们来看看执行过程。

  1. 首先,我们用函数生成器调用我们的运行器main
  2. 运行初始化我们的生成器,然后调用it.next()。这将控制权交给main
  3. Main 函数一直执行到yield。它会产生 的返回值addAsync,这是一个 Promise。目前,该 Promise 尚未实现。
  4. 现在控制权掌握在runner手中。它从 generator yield 中解开值并获取 Promise。它添加了一个.then,将已完成的 Promise 的值传递给main
  5. 每当承诺得到解决并且运行器将控制权交给时main,yield 表达式就会计算承诺的已解决值 (5),并继续执行直到结束。

处理非 Promises 值

目前,我们的runner期望收到一个Promise。然而,根据规范,你可以 await 任何值,无论是否是Promise。幸运的是,解决这个问题非常简单。

考虑以下同步添加函数:

同步添加函数

这段代码导致生成器崩溃,因为我们的生成器试图调用 a.then来获取生成的值。我们可以通过使用 来解决这个问题Promise.resolvePromise.resolve(arg)如果 arg 是Promise,则复制它;否则,将 arg 包装在Promise中。因此,我们的运行器变成了:

Runner 现在可以处理非 Promise 值

现在我们的代码不会因非 Promise 值而崩溃:

使用同步函数执行

如果我们用运行我们的代码addAsync,我们将获得与以前相同的行为!

处理错误

由于我们使用了Promises,因此我们可以轻松获取异步操作中发生的任何错误/拒绝。每当发生 Promise 拒绝时,我们的运行器只需解开拒绝原因并将其传递给生成器即可进行处理。我们可以使用以下方法来实现.throw

错误处理示例

现在,我们不仅添加了一个.then,还.catch向 yielded 的Promise添加了,如果发生拒绝,我们会将原因抛出到main。请注意,这也处理了我们正在执行同步操作并且存在正常 的情况throw。由于我们的运行器位于main执行堆栈的下方,因此此错误将首先冒泡到 in ,yieldmain在 中处理try...catch。如果没有try...catch,那么它就会冒泡到运行器,由于我们的运行器没有任何,它会再次冒泡,与async/awaittry...catch中相同

处理多个收益

我们已经取得了很大进展。现在我们的代码能够处理一个。由于我们使用了Promisesyield我们的代码已经能够运行多个并行异步操作,因此其他方法都是免费的。然而,我们的运行器无法运行多个语句。以下面的生成器为例:Promise.allyield

具有多个产量的发电机

我们的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
PREV
10 个炫酷的 CSS 文本效果
NEXT
什么是 GraphQL?