深入 Promise

4,713 阅读7分钟
> new Promise((resolve, reject) => setTimeout(resolve, 1000, 'foo'))  
> .then(console.log)  
> // foo (1s后)

在使用 Promise 的时候,我们最简单的理解与用法就是像上面的代码那样,把异步结果提供给 resolve 作参数,然后通过给 then 方法传递一个自定义函数作为结果处理函数。但 resolve 和 reject 这两个参数到底是什么?在这背后,它的基本工作方式到底是怎样的呢?让我们从规范的角度来初步了解它吧。

参考: ES8 Promise

TL;DR

  • promise 的工作机制与 callback 类似,都采用内部的抽象操作 Job 来实现异步
  • Promise 构造函数里的 resolve/reject 函数是内部创建的,在调用它们时传入的参数就是要解析的结果,把它和 promise 已经存储的用户传入的处理函数一起插入到 Job 队列中。传入的参数也可以是一个 promise,在 Promise.all/race 的内部就有用到。
  • Promise.prototype.then 根据当前的 promise 的状态来决定是立即将 promise 中存储的结果取出并和参数中的处理函数一起直接插入到 Job 队列中还是先与 promise 关联起来作为结果处理函数。then 会隐式调用 Promise 构建函数构建新的 promise 并返回。
  • Promise.all 先创建一个新的 promise,然后先、初始化一个空的结果数组和一个计数器来对已经 resolve 的 promise进行计数,之后会进行迭代,对于每个迭代值它都会为其创造一个promise,并设定这个promise的then为向结果数组里添加结果以及计数器--,当计数器减至0时就会resolve最终结果。
  • Promise.race 也是会创建一个新的主 promise,之后主要是根据 promise 只能 resolve 一次的限制,对于每个迭代值都会创造另一个promise,先resolve的也就会先被主 promise resolve 返回结果。

new Promise(executor)

首先从 Promise 这个构造函数说起,它是全局对象的 Promise 属性的值,这也就是为什么浏览器环境下我们能直接调用它的原因,就像 String, Array 这些构造函数一样。

new Promise(executor)的第一步就像其他构造函数一样,按照 Promise 的 prototype 来构建一个新对象,并初始化了几个内部插槽[[PromiseState]][[PromiseResult]][[PromiseFullfillReactions]][[PromiseRejectReactions]][[PromiseIsHandled]]来记录一些相关的信息,可以从名字来大致推断出他们的作用,详情我们下文再提。这里它们的初始值除了[[PromiseResult]]依次为 "pending",空 list,空 list,false。

下一步,ES 会根据这个 promise 对象来生成用来resolve promise的 resolve function 和用来 reject promise 的 reject function。然后调用 executor,以 resolve functionreject function 为参数,如果在这个过程中出错了,就直接 reject promise。最后返回 promise。

那什么又是 resolve,什么又是 reject 呢。我们知道 Promise 的状态,也就是[[PromiseState]]有三种值: pending, fullfilled, rejected,用 reject function 就可以 reject promise,把它的状态从 pending 变为rejected。不过 resolve function 既可以 fullfill promise 来把promise的状态从 pending 变为 fullfilled,也可以用来 reject promise。

那么 resolve functionreject function 到底做了些什么呢?

先来看 reject function ,首先在生成它的时候,会给它初始化[[Promise]][[AlreadyResolved]]插槽,也就是把它和某个 promise 关联起来。在执行时,会传入一个参数 reason,并只有当[[AlreadyResolved]]是 false,也就是还没 resolve 过、状态为 pending 时,才会调用返回 RejectPromise、传入 promise 和 reason 参数来 reject promise,否则返回 undefined。
RejectPromise(promise, reason),除了把[[PromiseState]]从 pending 变为 rejected 之外,还会把 promise 的结果[[PromiseResult]]的值设为 reason,并会取出 promise 的[[PromiseRejectReactions]]中已存的记录(相信读者们已经明白后面还会有一个操作来向这个内部插槽里存记录),并用 TriggerPromiseReactions 调用这些记录做后续处理,并传入 reject 的原因 reason。类似的,resolve function 中用到的 FullfillPromise(promise, value) 操作把 promise 的状态变为 fulfilled,抽取[[PromiseFullfillReactions]]的值调用 TriggerPromiseReactions,并传入 fulfilled 的结果 value。

TriggerPromiseReactions(reactions, argument) 会调用 EnqueueJob("PromiseJobs", PromiseReactionJob, <<reactions, argument>>),待会再详细说明。

再来看 resolve function,与 reject function 一样,在生成它时,会把它与某个 promise 关联起来。在执行时,我们传入的参数叫做 resolution。如果 promise 已经 resolve 过,就返回 undefined。之后的情况就相对复杂一些了。

  1. 如果用户把这个 promise 本身传给了 resolve function 作为参数 resolution,就会创建一个 TypeError,throw 它,并调用 RejectPromise,reason 参数为这个 TypeError。
  2. 如果 resolution 的类型不是 Object,就调用 FulfillPromise(promise, resolution)
  3. 其余的情况就是 resolution 是除了自身以外的带 then 的对象 (Promise) 的情况了。
    • 如果 resolution 是个不带then的对象,就 RejectPromise
    • 如果有 then 属性但不能调用,也 FulfillPromise, 。
    • 如果有 then 属性并且可以调用,就 EnqueueJob("PromiseJobs", PromiseResolveThenableJob, <<promise, resolution, thenAction>>)

在说明 EnqueueJob 之前,先来看看 Job 是个什么东西。简单来说,它就像是回调的内部实现机制:“当没有其他 ES 在跑时,初始化并执行自己对应的 ES。“。我们有一个待执行的 FIFO 的 Job 队列,以及当前的执行环境 running execution context 和 execution context stack,当后两者均为空时,才会执行 Job 队列的第一个。

ES 规定实现里至少要有两个 Job 队列,ScriptJobsPromiseJobs。当我们调用 EnqueueJob("PromiseJobs", ...)时,也就将要完成的 Job 和它们的参数插入到了 PromiseJobs 这个队列。可以看到,Promise 下有两种 Job

  1. PromiseReactionJob(reaction, argument)
    reaction 有三个内部插槽 [[Capability]][[Type]][[Handler]],分别表示 [[关联的 promise 及相关的resolve function 和 reject function]][[类别]][[handler]]。如果用户没有给 handler(undefined),就根据类别是 Fulfill 还是 Reject 来把 argument 当作结果。如果给了 handler,就用它来对 argument 进行进一步处理。最后根据这个结果来用 resolve function 和 reject function 进行处理并返回。
  2. PromiseResolveThenableJob(promiseToResolve, thenable, then)
    创建和 promiseToResolve 关联的 resolve function 和 reject function。以 then 为调用函数,thenable 为this,resolve function和reject function 为参数调用返回。

Promise.prototype.then(onfulfilled, onrejected)

首先是创建一个 promiseCapability,它包含了一个新的 promise 和相关联的 resolve functionreject function。promise 的产生就是像正常使用 Promise 构造函数那样构建一个 promise,不过传给构造函数 executor 是内部自动创建的,作用是把 resolve/reject function 记录到PromiseCapability中。 根据 promiseCapability 和 onfulfilled/onrejected 创建两个分别用于 fulfill 和 reject 的PromiseReaction,也就是 PromiseJobs 里最终要执行的操作。 如果当前的 promise(this)是 pending 状态,就把这两个 reaction 分别插入到 promise的[[PromiseFulfillReactions]][[PromiseRejectReactions]]队列中。但如果此时 promise 已经是 fulfilled 或是 rejected 状态了,就从 promise 的[[PromiseResult]]取出值 result,作为 fulfilled 的结果/reject 的原因,插入到 Job 队列里,EnqueueJob("PromiseJobs", PromiseReactionJob, <<reaciton, result>>),最后返回 prjomiseCapability 里存储的新 promise。Promise.prototype.catch(onrejected) 就是 Promise.prototype.then(undefined, onrejected)

Promise.resolve(x)

像 then 那样创建一个 promiseCapability,然后直接调用其中的 resolve function 并传入要解析的值x,最后返回其中的新 promise.

Promise.all(iterable)

Promise.all也会像 then 那样创建一个 promiseCapability,里面包含着一个新的 promise 及其关联的 resolve functionreject function,之后就结合迭代器循环:1.如果迭代完了并且计数器为0则调用 promiseCapabilityresolve function 来 resolve 结果数组 2.否则计数器加1,然后取出下一个迭代的值,传给 Promise.resolve 也构建一个新的 promise,然后内部创建一个 Promise.all Resolve Element Function,传给这个新 promise 的 then 用来把结果添加到结果数组并使计数器减一。

Promise.race(iterable)

同样的,创建一个 promiseCapability,然后进行迭代,用 Promise.resolve 来构建一个新的 promise,之后调用这个新 promise 的 then 方法,传入 promiseCapability 里的 resolve/reject function,结合之前提到的 promise 只会 resolve 一次,可以看到确实很有 race 的意味。

结语

看到这里,不知道大家是否对 Promise 有了更深的理解了呢。再往深一步,ES6里新提出的 async/await 实际上也是应用了 Generator 的思想与 Promise,感兴趣的话可以继续了解一下。


文 / Kacxxia

并没有作者介绍

本文已由作者授权发布,版权属于创宇前端。欢迎注明出处转载本文。本文链接:knownsec-fed.com/2018-08-22-…

想要看到更多来自知道创宇开发一线的分享,请搜索关注我们的微信公众号:创宇前端(KnownsecFED)。欢迎留言讨论,我们会尽可能回复。

感谢您的阅读。