十年老IT知识分享 – 答网友问:await 一个 Promise 对象到底发生了什么

大家好,我是二哥。

前两篇文章发出来后,有一些网友在后台咨询我一些问题,我把它们归总罗列在一起。这篇文章既是答网友问也是对前两篇的补充和复习。

先放下前两篇的链接。

图解 Node.js 的核心 event-loop

多图剖析公式 async=Promise+Generator+自动执行器

图 1:async 函数代码示例

问 0:上一篇所提到的 generator 和自动执行器是运行在不同的线程里面吗?

答 0:无论是 generator 还是自动执行器,都是在 event-loop 线程也就是运行 JS code 的主线程里面运行的。再强调一遍:它俩不是在两个线程里面运行的。

让我们再看一遍 Node.js 官网对 event-loop 的描述。它强调了一个重点:JS code 是以单线程方式被执行的。

The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system kernel whenever possible.

问 1:await p 这条语句产生了异步请求了吗?

答 1:不,它没有。await 只是在等待 p 状态的改变,无论状态是从 pending 变成 resolved 还是从 pending 变为 rejected 。

问 2:那异步请求是什么时候产生的?

答 2:是在 Promise 的 executor 里面,执行 setTimeout 时产生的。

下文把 new Promise() 时传递进去的 callback (resolve, reject)=>{ /* your code */} 称为 executor 。其中参数 resolve 和 reject 是由 Promise 自己实现的。需要注意的是这个 executor 是在 new Promise() 的时候,立即执行的。

假如我们在 executor 里面执行的是 fs.read(fd[, options], callback) 这样的语句,那类似地,异步请求是在调用 fs.read() 时产生的。

问 3:p 状态改变后,为什么通过 resolve(200) 传递的 200 会变成变量 res 的求值结果?

答 3:这就是为什么说我们需要了解 await 背后的实现原理。我们借助图 2 和图 4 来复习一下。

如图 2 所示,async 函数首先转换成了 generator 函数。但 generator 函数自己是不能自动运行的,所以得搭配一个自动执行器,驱动它往前走。自动执行器如同慈爱的妈妈,而 generator 就像那个懵懂的幼儿。小孩子每走一段路都会停下来,回头看看在他身后寸步不离的妈妈,得到妈妈的鼓励或者奖励后,再走向下一个目标。

图 2:async 函数转换成 generator 函数示例

在讲解图 4 之前,还是有必要再次复习两个重要的概念:yield 表达式和 yield 语句。如图 3 所示:

  • a+b 是表达式,它的求值结果影响到的是 { value: xxx, done: xxx } 中的 value 属性,而 { value: xxx, done: xxx } 是调用者通过迭代器调用 next() 方法的返回值 。
  • yield a+b 是 yield 语句,调用者可以通过给 next() 方法传实参来影响 yield 语句的返回值。比如 next(200) 则会使得变量 a1 为 200 。

图 3 还画出了一个重要的地方:generator 函数执行的暂停点:在 yield 表达式求值结束之后,但 yield 语句返回之前。

图 3:yield 表达式和 yield 语句对比

为了更好更清晰地回答问题 3,二哥给大家画了图 4 。

这一步开始通过执行器调用 generator。

虽然对 generator 真正的调用发生在这里,但 generator 函数在 ② 这步其实什么都没有做,只是立即返回了一个迭代器。

自动执行器从这里开始进入驱动 generator 模式。③ 这一步没有给形参 data 赋值,因为我们不能在第一次执行 g.next() 的时候给它注入一个值。

这一步每调用一次 g.next() 就会使得 generator 从上次暂停于 yield 的位置开始运行,直到再次遇到 yield 。

所以第一次对 g.next() 调用使得左侧 generator 函数从函数起始位置一直运行直到遇到 yield 。

我们看到 ⑤ 所标识出来的代码执行过程其实是创建了一个 Promise 对象,且在 Promise 的 executor 里面设置了一个 1s 钟的定时器。注意,这个 executor 是在创建 Promise 对象时立即执行的,不过 ⑦ 处的代码要等到 1s 之后才会执行。

generator 函数暂停之前,先会将 yield 表达式的求值结果通过 { value: xxx, done: xxx} 返回给 g.next() 调用方,也即右图 ④ 位置。

所以你一定猜到了,右图 ④ 位置的变量 result 为 { value: p, done: false} 。这里的 p 就是 ⑤ 执行过程中产生的 Promise 对象。

通过这样的方式,Promise 对象在 generator 函数和自动执行器之间流转。真是一个巧妙的过程。

那么你在右侧 ⑧ 处看到 result.value.then(callback) 这样的语句就不会感到纳闷了,这是 Promise 的标准用法。当 p 的状态变成 resolved 后,⑧ 处的 callback 自然就会得到运行的机会了。

1s 很快,滴答一下过去后,resolve(200) 得以运行。它的运行使得 p 的状态变成 resolved,所以在 ⑧ 处耐心等待的 callback 开始了它的工作。

是的,这个时候 data 的值为 200 。这是再自然不过的事,如果你对 Promise 的使用了然于胸的话。

自动执行器又一次执行 next(data) 。不过这一次给它传了一个实参 200 。所以这一次 ④ 处执行的代码变为: g.next(200)

自动执行器执行 g.next(200) 必然会驱使 generator 函数动身继续往前赶路。

还记得 generator 函数上次停在哪里休息的吗?对,左侧 ⑤ 处箭头所指的位置。generator 函数恢复运行后干的第一件事就是对 yield 语句求值。

如果像 g.next() 这样驱动它的话,yield 语句返回的是 undefined 。不过这次我们不一样,因为我们执行的是 g.next(200) 。很巧妙,传给 next() 的实参 200 作为 yield 语句的返回值赋值给了左侧变量 res 。

图 4:generator + 自动执行器细节图

让我们再回头看下图 1 的示例代码,我们来做个总结:

  1. await p 语句是个糖衣,它包裹的是 yield p 语句 + 自动执行器。
  2. 所谓 await p 暂停并不是说主线程执行 JS code 暂停了。相反主线程还在继续执行其它的 JS code 。
  3. await 是在等待 p 的状态发生变化。这个等待时间有多长?这完全取决于创建 p 的时候, executor 里面何时会调用 resolve() 或 reject() 。
  4. 执行 await p 语句的时候,无论 p 的状态是否已经发生了变化,执行到 await p 都会导致 V8 engine 转而去自动执行器里面执行。这是 yield p 语句使然。
  5. 自动执行器如同一个如影随形的妈妈,她拿到 p 之后,会耐心地等待,直到得到 p 状态改变后的 value 。最后再通过 g.next(value) 把 value 返回给它挚爱的 generator 函数。

图 5:同图 1

以上就是本文的全部内容。码字不易,画图更难。喜欢本文的话请帮忙转发或点击“在看”。您的举手之劳是对二哥莫大的鼓励。谢谢!

正文完