手把手教你实现 Promise/A+ 规范

什么是 Promise/A+ 规范?

这个问题可以拆解为三个子问题:

  1. 什么是 promise?
  2. 「Promise/A+」中的「A」是什么含义?
  3. Promise/A+」中的「+」是什么含义?

什么是 promise ?

我查阅了很多国外文档,对于「什么是 promise」 的这个定义,大部分都是这么说:

A promise represents the eventual result of an asynchronous operation.

从语义上来说,一个「promise」代表的是一个异步操作的未来结果;

从代码的角度来说,promise 就是一个普通的 js 对象(这个对象可以是字面量对象,也可以是通过构造得到的对象),这个对象必须具有一个叫做 then 的「方法」(注意,这里强调了「方法」,也就是说一个叫then的属性,它的值必须是 function 类型)。换句话说,这个 js 对象必须是 thenable 的。

「Promise/A+」中的「A」是什么含义?

实际上,promise 概念从萌芽到逐渐成型,它是经历很长的发酵和演化的过程的。在 2009 年之前,它都是停留在代码成的实现,没有人正式地对它进行定义。到了 2009 年,一个叫 Kris Zyp 的大佬正式将「promise」这个概念提到书面上来 - 也就是说他提出了关于 promise 的第一个提案

这篇 wiki 文章里面指出,除了「Promise/A」规范,还有其他称之为「Promise/B」,「Promise/C」和「Promise/D」的规范提案:

从这里我们得知,「Promise/A」是一个整体。它代表的是一个 promise 规范提案的名字。至于为什么称之为「A」呢?我估计是跟提案提出的时间点的先后有关系。也就是说,从时间轴上面来看:

  • 首先是「Promise/A」先提出;
  • 其次是「Promise/B」提出;
  • 再次是「Promise/C」提出;
  • 最后才是「Promise/D」提出;

当然,上面只是我的猜测,还需要考证。但是通过上面的考证,我们知道了,「Promise/A」是一份由 kris Zyp 所提出的 promise 规范提案。当前,它保存在这里

「Promise/A+」中的「+」是什么含义?

我们来到了最后一个子问题。根据我们对「+」,即英文的 「plus」在日常表达中应用的理解,不难推测它应该是在表达「更优,更高级」的意思。果不其然,我们在 「Promises/A+」的规范文档里面还真的验证了这个猜想:

Historically, Promises/A+ clarifies the behavioral clauses of the earlier Promises/A proposal, extending it to cover de facto behaviors and omitting parts that are underspecified or problematic.

也就是说,Promises/A+ 规范进一步澄清和做实了很多 Promises/A 规范没有说清楚的地方,并基于当时社区的应用现状做了进一步的优化和裁剪:

  • 优化 - 把当前社区对 promise 概念实现的最大公约数采纳进来;
  • 裁剪 - 把 Promises/A 规范没有表达明确的或者有问题的那些部分去掉。

综上所述,Promises/A+ 规范就是对 Promises/A 规范的一个改良版规范。所以,Promise/A+」中的「+」就是「改良版」的意思。

Promises/A+ 规范的诞生背景

在Promises/A+ 规范诞生之前,社区对于 「promise」概念(此时,大部分实现里面都称之为「deferred」)已经形成基本的共识,不过对于它的实现却也是五花八门。当这五花八门的 promise 实现类库混合到一块的时候,社区发现了一个很大的问题 - 不同的 promse 类库无法实现「互操」 。比如说,消费者无法在 A promise 类库的 then() 方法的 callback 里面返回一个 B promise 类库的 promise 对象。比如说,下面的代码是无法执行的:

js 复制代码
ALibPromise.then(()=>{
    return BLibPromise
});

所谓的「社区」,说到底就是一个民间自发组织的利益共同体。不统一性无疑是破坏了社区中所有人的利益最大化。

与此同时,正是 HTML5 规范和实现高歌猛进的时间节点。在 HTML5 中,有很功能模块(比如 indexDB, fetch API 等)都需要考虑如何处理和实现异步操作。在此之前,浏览器中对异步操作的实现也是五花八门。有些地方是采用 promise-like 方式式,有些地方是采用 callback 模式,还有地方是采事件模式。官方也需要探索出一个统一的方式来实现异步操作类型的 API。

在 Promises/A+ 规范诞生之前,Kris Zyp 提案的 Promises/A 规范已经存在了。尽管存在Promises/A 规范文档且该规范所要求的实现也比较简单,但是由于缺乏有力的约束条件,所以,大家的实现还是没有统一起来,因为无法实现相互操作,无法共存于一段程序里面。

Ember 框架的贡献者 [Paul Chavard] (github.com/tchak) 第一个提出了用一套测试用例去对 promise 的实现进行约束。

随后,来自于 google 的 web 标准专家 Domenic Denicola 受到了 Paul Chavard 的启发,并意识到 Kris Zyp 提案的 Promises/A 规范存在的不足性(没有贴近社区现状),于是乎,他写下了一份主要是服务于社区现状的改良版规范和一套与之配套的测试用例 - Promises/A+ Compliance Test Suite

这份改良版的Promises/A 规范就是我们现在称之为「Promises/A+ 规范」。所有能够通过 Promises/A+ 合规性测试的 promise 实现类库,我们就称之为「Promises/A+ 兼容的 promise 类库」。

Promises/A+ 规范诞生的意义

也许因为 Promises/A+ 足够精简和贴近社区实现,自从 Promises/A+ 规范公布后,社区一拥而上,纷纷作出响应。截止当前,官方公布的就有 50+ 类库或者框架实现了Promises/A+ 规范。至此,在社区中,我们就实现了各种 promise 实现的混用,因为不管你叫 async/deferred 还是 promise,相互之间都是因为实现了 Promises/A+ 而保证了在 .then()层面是互通互操的。简单来说都是 thenable 的。至此,社区因为这次的大一统而获得了巨大的利益最大化。

最重要的是,「存在多个遵循同一个规范而实现的 promise 类库」这个事实向 TC39 表明了,当前社区已经对 promise 概念已经形成共识了。这个事实直接推动了 TC39 官方采用 Promises/A+ 规范,并对它进行扩充最终落地到 ES6 规范中来。这对于社区来说,无疑是一场巨大的胜利。与此同时,对在标准规范制定方面的官民合作也提供了成功的范例。

现在回头来看,Promises/A+ 规范是 promise 概念一路在编程世界里面高歌猛进且跌宕起伏这个历程里面的高潮部分。它如此重要,我们禁不住要看看它的庐山真面目。不过,这里,我就不直接把规范文档直接摆上来,而是对它进行概括性描述。

Promises/A+ 规范的简要描述

总览全文来看,Promises/A+ 规范志在实现促成当前多个 promise 实现的互促性的这个目标的前提下,尽可能地让规范标准保持精简 。因为,不同 promise 实现之间的互操主要是体现在 then() 方法上,所以,Promises/A+ 规范的主体内容就是详细描述then() 方法的行为标准。

上面的描述是对 Promises/A+ 规范在旨意上的阐述。下面对各个部分进行简要描述。

Promises/A+ 规范总共有可以分为六部分:

  1. 前言;
  2. 术语;
  3. promise state 的描述与定义;
  4. then() 方法行为标准的描述;
  5. promise 对象的 resolve 程序;
  6. 标注解释。

前言

前言这里主要是表达一下几个信息:

  1. 第一点,给 「promise」这个概念在语义上下定义;

A promise represents the eventual result of an asynchronous operation.

  1. 第二点,因为已经决定通过 then() 方法来进行不同 promise 实现之间的互操,同时为了保持规范文档的精简和易于实现,所以,本文档的主要任务是描述 then() 方法的行为标准,并不会包含规定如何创建,fulfill 或者 reject 某个 promise 这方面的内容;
  2. 第三点,介绍了 Promises/A+ 规范是对 Promises/A 规范的进一步补充和改良;

术语

术语这部分内容很少,总共给出了五个术语的定义:

  • "promise"
  • "thenable"
  • "value"
  • "exception"
  • "reason"

"promise"

这里给出了「promise」在代码层面的定义。

一个 js 对象(广义上的「js 对象」),只有你有一个叫做 then 的方法(注意,这里强调的是「方法」而不是属性,意思就是 then 是一个函数),并且该方法的行为表现符合本规范所要求的标准,那么这个 js 对象就是一个 「promise」。

这种定义的方式类似于 js 在类型系统里面的思想:「鸭子辩型」。因为在 js 里面,其实一切皆「对象」。从这个角度来说,一个函数也可能是一个「promise」。

"thenable"

一个 js 对象(狭义,指的是字面量对象或者构造出来的对象)或者函数,只要它有一个叫做 then 的方法,我们就称之为「thenable」。

"value"

promise 的最终值可以是任何合法的 js 值,包括比较特殊的 undefined,promise 类型或者 thenable。

这里为后面的规范内容埋下了伏笔。万一 promise A 的最终值又是另外一个 promise B 呢? 对于这种情况,我们该如何处理呢 ?是把这个 promise B 当做最终值返回给用户还是说继续等待,等待 promise B 出了最后的结果,再把这个结果返回给用户呢?

Promises/A+ 规范选择了后者。也就说,Promises/A+ 规范认为「promise 应该要被拍平」,拍到直到某个 promise 的最终值不是 promise 类型(严格来说是 thenable)的值为止。

关于这部分,Promises/A+ 规范会在「The Promise Resolution Procedure」那部分进行详细阐述。

"exception"

在 promise 语境下,Promises/A+ 规范认为只要使用 throw 语句来抛出的值就是 "exception"。可想而知,字符串字面量也是一个合法的异常。因为 throw 关键字后面可以跟着任何 js 表达式。

不过,约定俗成而言,我们还用抛出一个 JS Error 对象的实例是比较合理的。

"reason"

一般而言,reason 是一个用于表示异常原因的字符串文本。也就是说是上面实例化 Error 对象时候传入的字符串。

当然,从 Promises/A+ 规范来说,只要你在语义上描述了异常的原因,任何合法的 js 值都可以是 "reason"。

promise state 的描述与定义

这里规范要求 promise 有两个生命阶段:

  1. 首先是 unsettled 阶段;
  2. 然后是 settled 阶段;

这两个生命阶段会对应三个状态:

  1. unsettled 阶段,promise 的状态称之为「pending」;
  2. settled 阶段,promise 的状态有可能是「fulfilled」也有可能是「rejected」。

与此同时,规范强调了两点 :

  • 一个 promise 一旦 settled 下来后,它的状态和值都是不可变的
  • 值的不可变是指在「引用层面」的不可变。也就是说拿 === 全等操作符去做判断,其实需要为 true

then() 方法行为标准的描述

用途

then() 方法主要是用于获取 promise 对象 settled 下来时候的值。这个值有可能是 fulfilled 时候具有成功含义的值,也有可能是 rejected 含义的 reject 原因。

函数签名

从这里我们可以明确地看到,在 promise/A+ 规范中,onFulfilledonRejected 这两个 callback 函数并不是必传的,可以不传。所以,我们在实现的时候,需要注意判断是否传递 callback 函数的情况。

onFulfilledonRejected callback 的调用时机与细节

onFulfilledonRejected 这两个 callback 都要遵守下面的调用时机和回传实参的细节要求:

  • 只有在 promise 的状态变为「fulfilled」之后,才能调用 onFulfilled ;只有 promise 的状态变为「rejected」之后,才能调用 onRejected
  • 调用 onFulfilled 的时候,具有成功含义的值要作为第一个实参传入;调用 onRejected 的时候,具有失败原因含义的值要作为第一个实参传入;
  • 一个 onFulfilledonRejected 函数只能被调用一次。
  • 必须要等用户的同步代码执行完毕,才能调用这两个 callback 函数。换句话说,onFulfilledonRejected 需要以异步的方式去调用;
  • onFulfilledonRejected 不需要绑定 this 的指向。

对同一个 promise 对象,可以反复通过 then 来监听和检索它的未来值

在这种情况下,调用 then() 方法入队 onFulfilled 或者 onRejected callback 的顺序就是它们调用的调用顺序。

then 方法要支持链式调用以及如何处理上游 promise 与下游 promise 之间的关系

此处,promise1 对象的 onFulfilled 或者 onRejected callback 所返回的 thenable 类型的值称之为「上游 promise」,promise2 被称之为「下游 promise」

这一部分描述的就是如果基于 then() 方法的 callback 的返回值来执行不同 promise 实现之前的互操能力。

从这段规范的描述我们可以得知,基于 then() 方法的互操能力是由三个环节组成:

  1. then 方法具备链式调用的能力,故而存在上游 promise 和 下游 promise;
  2. 如果 onFulfilled/onRejected 不传递的话,那么下游 promise 的状态和值就等同于上游 promise 的状态和值;
  3. 如果 onFulfilled/onRejected 有返回值的话,那么下游 promise 的状态和值就由「promise Resolution Procedure」算法(简记为:[[Resolve]](下游 promise 对象, callback 返回值))的结果来决定。

promise 对象的 resolve 程序

[[Resolve]](下游 promise 对象, callback 返回值) 算法,基本上可以描述为:"如果 callback 返回值为非 thenable 对象,那么下游 promise 对象的最终值就是这个 callback 的返回值;否则的话,程序就需要等待这个thenable 对象最终 settled 下来,拿这个最终值来来充当下游 promise 对象的最终值"。

这段规范内容阐述了采用这种算法的初衷是:

  1. 扩大各个 promise 实现的互操的可能性;
  2. 为 Promise/A+ 兼容的实现提供了「同化非Promise/A+ 兼容的实现」的能力。

剩下的部分就可以划分为一个防守和三个 case。哪三个 case呢?

  • callback 返回值是一个真正的 promise 对象;
  • callback 返回值是一个 thenable 对象;
  • callback 返回值是排除上面两种情况的其他类型的值;

防守

做这个防守处理是为了避免无限递归。

返回值是一个真正的 promise 对象

这里要表达的意思就是上面提到过的:「下游 promise」的值和状态跟「上游 promise」的值和状态是保持一致

返回值是一个 thenable 对象

这种情况下,还是要防守- 「即需要判断 then 属性值是不是一个函数类型」

then 属性值是一个函数类型值

先把 then 方法的引用保存下来,以防被外部覆盖或者篡改了。

  1. 调用 then() 方法的时候,把 this 绑定为上游 promise 的 then callback 的返回值;
  2. 在调用用户所提供的 then() 方法的时候,我们传入的第一个实参是 resolvePromise 函数,第二是实参是 rejectPromise 函数(目的是让用户自己决定 thenable 的最终状态和值);
  3. 如果用户多次调用 resolvePromise 函数或者 rejectPromise 函数,又或者同时调用了 resolvePromise 函数和rejectPromise 函数,那么这种情况下以最先调用的那一次函数为准,用户后面的调用都要忽略;
  4. 调用 then() 方法的时候遇到抛出的异常,如果此时 rejectPromise 函数或者 rejectPromise 函数都还没有调用,那么就把捕获的那个异常作为下游 promise 的 reject reason;否则,忽略这个异常;

从上面的描述来看,Promise/A+ 规范把用户的 thenable 对象的 then() 方法看作为真正 promise 的 executor() 函数来处理。

then 属性值不是一个函数类型值

这种情况,直接把 callback 返回值当做下游 proimse 对象的值即可。

返回值是其他类型的值

这种情况,直接把 callback 返回值当做下游 proimse 对象的值即可。

手写 Promise/A+ 规范的实现

熟悉 Promise/A+ 规范后,下面我就着手去手写它的实现。如果把完整实现 Promise/A+ 规范的代码看作一个整体,那么在我看来,这个整体可以根据难度划分为三个依次递进的部分:

  • 基础部分 - 构造函数的实现;
  • 进阶部分 - then()方法的实现;
  • 高阶部分 - promise resolve 算法的实现。

构造函数的实现

针对这部分的实现,Promise/A+ 规范着重指出了几点:

  • 有两个阶段,三个状态。状态的转化路径有两个:
    • pending -> fulfilled
    • pending -> rejected
  • 状态一旦 settled 下来后,它是不能改变的。promise 的 fulfill 值和 reject 原因也是一样。

上面的重点其实是指出了 statevalue等属性的外部不可访问性(因为一旦可以访问了,那么外部就可以去改)。

A promise must provide a then method to access its current or eventual value or reason. Promise/A+ 规范指出了,要想访问 promise 的最终值,只能通过 then() 来注册 callback(onFulfilled 或者onRejected)来实现。也就说,我们可以禁止通过 promise.value 来访问 promise 的最终值。

鉴于上面的标准要求,我们用 class 的私有属性来实现是十分恰当的。

我们把要实现的 class 名称叫做 MyPromise。下面是它的构造函数实现:

js 复制代码
class MyPromise {
  #value = undefined;
  #reason = undefined;
  #state = "pending";
  #onFulfilledCallbacks = [];
  #onRejectedCallbacks = [];

  constructor(executor) {
    const resolve = (value) => {
      // 这个判断条件是用来方式用户多次调用 `resolve()`
      if (this.#state === "pending") {
        this.#state = "fulfilled";
        this.#value = value;
        this.#notifyAllListenters(value, this.#onFulfilledCallbacks);
      }
    };

    const reject = (reason) => {
      // 这个判断条件是用来方式用户多次调用 `reject()`
      if (this.#state === "pending") {
        this.#state = "rejected";
        this.#reason = reason;
        this.#notifyAllListenters(reason, this.#onRejectedCallbacks);
      }
    };

    try {
      // 用户传给我们的 executor 最终是被我们调用。
      // 调用的时候,把 `resolve` 和 `reject` handler 回传给用户。由用户来决定 promise 的最终状态和值。
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  #notifyAllListenters(val, callbacks) {
    for (const callback of callbacks) {
      if (typeof callback === "function") {
        callback(val);
      }
    }
  }
}

上面在代码中,我也指出了,因为我们把 resolvereject handler 回传给用户了。秉持着「永远都不要相信用户的输入」的原则,我们还是要做防守 - 防止用户多次调用 resolve() 或者 reject()

另外的一个逻辑的就是,promise 的状态一旦是 settled 下来后,那么我们就需要马上通知所有通过then() 注册进来的 onRejected 或者 onFulfilled callback。这里用到的就是「发布-订阅者」模式。从这个角度来理解,其实 then() 就是事件模式里面的 addEventListener(),它监听的是 promise 最终值 settled 的那一刻。

现在,我们可以尝试用 MyPromise 去创建一个 promise 实例对象:

Finally, the core Promises/A+ specification does not deal with how to create, fulfill, or reject promises ...

其实,在 Promise/A+ 规范中并没有规定怎么创建,resolve 和 reject promise。所以,上面的实现其实遵循的是 ES6 中的 promise 标准规范。

then()方法的实现

根据规范内容,我们知道,then()方法的签名是:

js 复制代码
then: (onFulfilled: (v: any) => any, onRejected: (r: any) => any) => MyPromise;

于是乎,我们不难写出下面的代码:

js 复制代码
class MyPromise {
    ...

    then(onFulfilled, onRejected){
        const promise2 = new MyPromise((resolve, reject) => {
            // 待实现
        });

        return promise2
    }
}

就拿 onFulfilled 来说,规范规定以下几点:

  • 它是可以可选的。也就是说用户可以不传递这个参数。即使传递了,我们也要检验它是不是函数类型;
  • 规定了它的被调用时机:「它只能在它监听的 promise 对象的状态 settled 下来(并且状态值是 fulfilled)」之后被调用;
  • 规定了它的被调用方式:需要被「异步」调用
  • 规定了它的被调用次数:只能被调用一次。

又因为,onFulfilled() 的返回值会关系到 then() 方法所返回的 promise2 对象的 resolve 逻辑。所以,调用 onFulfilled() 逻辑需要放在 promise2 对象的 executor 里面。

与此同时,我们需要考虑,如果调用 then() 的时候,promise 的状态还是 pending 该怎么办?答案是:「我们应该要把 fulfilled 下对onFulfilled 的处理包成一个新的 onFulfilled,然后把它推入到 onFulfilledCallbacks,等待用户 resolve promise 那一刻的到来。」

经过上面的分析,我们可以得到处理 onFulfilled 的框架:

js 复制代码
class MyPromise {
    ...

    then(onFulfilled, onRejected){
        const promise2 = new MyPromise((resolve, reject) => {
            if(this.#state === 'fulfilled'){
                // 这种情况,可以马上调用 `onFulfilled()`
            }else if(this.#state === 'pending'){
                // 这种情况下,需要把 `onFulfilled` 推入到队列中,延迟到 promise 被用户 resolve 的时候再处理
            }
        });

        return promise2
    }
}

在上面的这两种情况下,因为对 onFulfilled 的处理都是一样的,所以,我们可以把相同的代码抽成一个叫做函数 handleOnFulfilled:

js 复制代码
class MyPromise {
    ...

    then(onFulfilled, onRejected){
        const promise2 = new MyPromise((resolve, reject) => {
            const handleOnFulfilled = () => {
                if (typeof onFulfilled === "function") {
                  this.#asyncCall(() => {
                    try {
                      const result = onFulfilled(this.#value);
                      this.resolvePromise(promise2, result, resolve, reject);
                    } catch (error) {
                      reject(error);
                    }
                  });
                }
             };
             
            if(this.#state === 'fulfilled'){
                // 这种情况,可以马上调用 `onFulfilled()`
                handleOnFulfilled()
            }else if(this.#state === 'pending'){
                // 这种情况下,需要把 `onFulfilled` 推入到队列中,延迟到 promise 被用户 resolve 的时候再处理
                this.#onFulfilledCallbacks.push(handleOnFulfilled);
            }
        });

        return promise2
    }
}

因为上面也提到了,Promise/A+ 规范要求我们要以 『异步』的方式调用 onFulfilled,所以,我们得实现上面的那个 asyncCall() 方法。就目前已经落地的 ES6 规范而言,onFulfilled 是放在 microtask 队列里面,这里我们使用 queueMicrotask() 方法来模拟。

js 复制代码
class MyPromise {
    ...

    #asyncCall(fn){
        queueMicrotask(fn)
    }

    then(onFulfilled, onRejected){
        const promise2 = new MyPromise((resolve, reject) => {
        
               const handleOnFulfilled = () => {
                if (typeof onFulfilled === "function") {
                  this.#asyncCall(() => {
                    try {
                      const result = onFulfilled(this.#value);
                      this.resolvePromise(promise2, result, resolve, reject);
                    } catch (error) {
                      reject(error);
                    }
                  });
                }
             };

            if(this.#state === 'fulfilled'){
                // 这种情况,可以马上调用 `onFulfilled()`
                handleOnFulfilled();
            }else if(this.#state === 'pending'){
                // 这种情况下,需要把 `onFulfilled` 推入到队列中,延迟到 promise 被用户 resolve 的时候再处理
                this.#onFulfilledCallbacks.push(handleOnFulfilled);
            }
        });

        return promise2
    }
}

对于 onFulfilled 的处理已经阐述并实现完毕。因为 pending -> rejected 是平行于 pending -> fulfilled 的另一个状态转化 case。所以,对于 onRejected 的处理我们可以同理可得(唯一的不同点是,调用onRejected() 回传给用户的值是 this.#reason,而不是this.#value):

js 复制代码
class MyPromise {
  ...
  
  #asyncCall(fn) {
    queueMicrotask(fn);
  }

  then(onFulfilled, onRejected) {
    const promise2 = new MyPromise((resolve, reject) => {
      const handleOnFulfilled = () => {
        if (typeof onFulfilled === "function") {
          this.#asyncCall(() => {
            try {
              const result = onFulfilled(this.#value);
              this.resolvePromise(promise2, result, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        }
      };

      const handleOnRejected = () => {
        if (typeof onRejected === "function") {
          this.#asyncCall(() => {
            try {
              const result = onRejected(this.#reason);
              this.resolvePromise(promise2, result, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        }
      };

      if (this.#state === "fulfilled") {
        // 这种情况,可以马上调用 `onFulfilled()`
        handleOnFulfilled();
      } else if (this.#state === "rejected") {
        handleOnRejected();
      } else if (this.#state === "pending") {
        // 这种情况下,需要把 `onFulfilled` 推入到队列中,延迟到 promise 被用户 resolve 的时候再处理
        this.#onFulfilledCallbacks.push(handleOnFulfilled);

        // 同上
        this.#onRejectedCallbacks.push(handleOnRejected);
      }
    });

    return promise2;
  }
}

聪明如你,一定看到上面的代码中还有一个 resolvePromise() 的方法没有实现。它就是下游 promise 的 resolve 算法。

根据 Promise/A+ 规范的描述我们知道,该算法关心的是上游的 onFulfilled 或者 onRejected callback 的返回值。由 callback 的返回值决定了下游 promise 何时和怎样 resolve。

在规范中,它把上面两个 callback 的返回值区分三种情况:

  • 真正的 promise - 当前的 promise 对象是 MyPromise 的实例;
  • 普通 js 对象和函数 - 它们的身上可能有一个叫做 then 的属性。假如,它有这么一个属性,我们还要根据判断它的属性值是不是函数值;
  • 其他类型的普遍 js 值 - 排除了上面两种情况之外的其他任意类型的 js 值。

根据上面的分析,我们可以写出 resolvePromise() 实现的框架:

js 复制代码
class MyPromise {
  ...
  #resolvePromise(promise, x, resolve, reject) {
    // 2.3.1. If promise and x refer to the same object, reject promise with a TypeError as the reason.
    if (promise === x) {
      reject(new TypeError("Chaining cycle detected for promise"));
    }

    if (x instanceof MyPromise) {
      //...
    } else if (
      (typeof x === "object" && x !== null) ||
      typeof x === "function"
    ) {
      //...
    } else {
      // 其他类型的普遍 js 值的情况下,直接把原来的值 resolve 出去即可
      resolve(x);
    }
  }
}

类似于考试循序先易后难,我们可以把简单的代码都先写了:

  • 2.3.1.1 条例让我们先做个防止循环引用的防守;
  • 其他类型的普遍 js 值的情况下,直接把原来的值 resolve 出去即可。

然后,我们先来处理第一种情况:「上游 promise 的 callback 函数的返回值为真正的 promise 类型」。

2.3.2 条例基本上是在讲:这种情况下,下游 promise 的状态跟值由 callback 返回的 promise 对象的状态和值决定。所以,我们需要在 callback 返回的 promise 对象 settled 的时候再去 resolve 或者 reject。换句话说,我们需要通过它的 then() 方法来监听它进入 settled 的那一瞬间。

所以,我们很快就有下面的实现:

js 复制代码
class MyPromise {
    ...
    #resolvePromise(promise, x, resolve, reject){
        // 2.3.1. If promise and x refer to the same object, reject promise with a TypeError as the reason.
        if (promise === x) {
          reject(new TypeError("Chaining cycle detected for promise"));
        }

        if(x instanceof MyPromise){
            x.then(
                (value)=>{ resolve(value)},
                (reason)=>{ reject(value)},
            )
        }else if((typeof x === 'object' && x !== null) || typeof x === 'function'){
            ...
        }else {
            // 其他类型的普遍 js 值的情况下,直接把原来的值 resolve 出去即可
            resolve(x)
        }
    }
}

但是,细想之下,我们显然是忽略了 x 在 settled 时候,回传给我们的 value 或者 reason 还是 thenable 的情况。聪明如你,一下子想到了递归的想法。利用递归 resolve ,我们可以找到那个最终的普通值。

js 复制代码
class MyPromise {
    ...
    #resolvePromise(promise, x, resolve, reject){
        // 2.3.1. If promise and x refer to the same object, reject promise with a TypeError as the reason.
        if (promise === x) {
          reject(new TypeError("Chaining cycle detected for promise"));
        }

        if(x instanceof MyPromise){
            x.then(
                (value)=>{ this.#resolvePromise(promise,value,resolve, reject)},
                (reason)=>{ this.#resolvePromise(promise,reason,resolve, reject)},
            )
        }else if((typeof x === 'object' && x !== null) || typeof x === 'function'){
            ...
        }else {
            // 其他类型的普遍 js 值的情况下,直接把原来的值 resolve 出去即可
            resolve(x)
        }
    }
}

最后,我们还有一个 case 没有处理,那就是「callback 返回值是普通 js 对象和函数」的情况。

老规矩,我们来分析一下 2.3.3. 条例。该条例指出在这个 case 里面,我们又可以划分为三个小 case:

  1. then 属性值是函数类型;
  2. then 属性值不是函数类型;
  3. 如果 then 属性值是函数类型,那么假如在执行它的过程中出错的话,那么捕获的那个错误对象就是下游 promise 对象的 reject 原因。

综上所述,我们可以得到下面的框架代码:

js 复制代码
class MyPromise {
    ...
    #resolvePromise(promise, x, resolve, reject){
        // 2.3.1. If promise and x refer to the same object, reject promise with a TypeError as the reason.
        if (promise === x) {
          reject(new TypeError("Chaining cycle detected for promise"));
        }

        if(x instanceof MyPromise){
            x.then(
                (value)=>{ this.#resolvePromise(promise,value,resolve, reject)},
                (reason)=>{ this.#resolvePromise(promise,reason,resolve, reject)},
             )
        }else if((typeof x === 'object' && x !== null) || typeof x === 'function'){
            let then = x.then;

            try{
                if(typeof then === "function"){
                    ...
                }else {
                    resolve(x)
                }
            }catch(e){
                reject(e)
            }
        }else {
            // 其他类型的普遍 js 值的情况下,直接把原来的值 resolve 出去即可
            resolve(x)
        }
    }
}

针对第一个 case,通过仔细体会规范的描述,这种情况下的 then() 方法并不是等同于这真正 promise 对象的 then()方法,而是被认为扮演的是 executor的角色 。所以,我们需要传递 resolve 或者 reject 两个 handler 来给 thenable 对象的拥有者,让它承担像创建真正 promise 实例对象时候所用到的 executor 的职责。

If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).

最后最重要的一点是,在条例 2.3.3.3.1 中,规范指出,我们需要考虑 resolve 值是非普通对象的情况。

综上所述,我们对第一个 case 的实现代码如下:

js 复制代码
...
if (typeof then === "function") {
    then(
        (y)=>{
            this.#resolvePromise(promise, y, resolve, reject);
        },
        (r)=> {
            reject(r);
        }
    )
}
...

来到这里,我们其实已经根据 Promise/A+ 规范的要求实现了 promise 的 resolve 算法。

不过,仔细复盘一下,我们还漏了两个细节:

  1. 条例 2.3.3.3.3 明确指出,我们需要防范我们提供给用户的 resolvereject handler 被多次调用或者两者同时调用的非法情况;
  2. 条例 2.3.3.3. 明确指出,then() 方法调用的时候, this 需要绑定为 x

所以,针对上面第一个遗漏的细节,我们创建一个标志位 isCalled 来做防守;针对第二个细节,我们简单利用 call 来把 this 绑定到 x 上面来即可。

我们继续完善一下我们的代码:

js 复制代码
class MyPromise {
   ...
  #resolvePromise(promise, x, resolve, reject) {
    // 2.3.1. If promise and x refer to the same object, reject promise with a TypeError as the reason.
    if (promise === x) {
      reject(new TypeError("Chaining cycle detected for promise"));
    }

    if (x instanceof MyPromise) {
      x.then(
        (value) => {
          this.#resolvePromise(promise, value, resolve, reject);
        },
        (reason) => {
          reject(reason);
        }
      );
    } else if (
      (typeof x === "object" && x !== null) ||
      typeof x === "function"
    ) {
      let then = x.then;
      let called = false;
      try {
        if (typeof then === "function") {
          then.call(
            x,
            (y) => {
              if (called) return;
              called = true;
              this.#resolvePromise(promise, y, resolve, reject);
            },
            (r) => {
              if (called) return;
              called = true;
              reject(r);
            }
          );
        } else {
          resolve(x);
        }
      } catch (e) {
        if (!called) {
          called = true;
          reject(e);
        }
      }
    } else {
      // 其他类型的普遍 js 值的情况下,直接把原来的值 resolve 出去即可
      resolve(x);
    }
  }
}

总结

将上面三个阶段的代码整理到一块,我们可以得到实现 Promise/A+ 规范规范的所有代码:

js 复制代码
class MyPromise {
  #value = undefined;
  #reason = undefined;
  #state = "pending";
  #onFulfilledCallbacks = [];
  #onRejectedCallbacks = [];

  constructor(executor) {
    const resolve = (value) => {
      // 这个判断条件是用来防止用户多次调用 `resolve()`
      if (this.#state === "pending") {
        this.#state = "fulfilled";
        this.#value = value;
        this.#notifyAllListenters(value, this.#onFulfilledCallbacks);
      }
    };

    const reject = (reason) => {
      // 这个判断条件是用来防止用户多次调用 `reject()`
      if (this.#state === "pending") {
        this.#state = "rejected";
        this.#reason = reason;
        this.#notifyAllListenters(reason, this.#onRejectedCallbacks);
      }
    };

    try {
      // 用户传给我们的 executor 最终是被我们调用。
      // 调用的时候,把 `resolve` 和 `reject` handler 回传给用户。由用户来决定 promise 的最终状态和值。
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  #notifyAllListenters(val, callbacks) {
    for (const callback of callbacks) {
      if (typeof callback === "function") {
        callback(val);
        this.#resolvePromise;
      }
    }
  }

  #asyncCall(fn) {
    queueMicrotask(fn);
  }

  then(onFulfilled, onRejected) {
    const promise2 = new MyPromise((resolve, reject) => {
      const handleOnFulfilled = () => {
        if (typeof onFulfilled === "function") {
          this.#asyncCall(() => {
            try {
              const result = onFulfilled(this.#value);
              this.#resolvePromise(promise2, result, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        }
      };

      const handleOnRejected = () => {
        if (typeof onRejected === "function") {
          this.#asyncCall(() => {
            try {
              const result = onRejected(this.#reason);
              this.#resolvePromise(promise2, result, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        }
      };

      if (this.#state === "fulfilled") {
        handleOnFulfilled();
      } else if (this.#state === "rejected") {
        handleOnRejected();
      } else {
        // pending
        if (typeof onFulfilled === "function") {
          this.#onFulfilledCallbacks.push(handleOnFulfilled);
        }

        if (typeof onRejected === "function") {
          this.#onRejectedCallbacks.push(handleOnRejected);
        }
      }
    });

    // 2.2.7. then must return a promise
    return promise2;
  }

  #resolvePromise(promise, x, resolve, reject) {
    if (promise === x) {
      reject(new TypeError("Chaining cycle detected for promise"));
    }

    if (x instanceof MyPromise) {
      x.then(
        (value) => resolvePromise(promise, value, resolve, reject),
        (reason) => reject(reason)
      );
    } else if (
      x !== null &&
      (typeof x === "object" || typeof x === "function")
    ) {
      let then = x.then;
      let called = false;

      try {
        if (typeof then === "function") {
          then.call(
            x,
            (y) => {
              if (called) return;
              called = true;
              resolvePromise(promise, y, resolve, reject);
            },
            (r) => {
              if (called) return;
              called = true;
              reject(r);
            }
          );
        } else {
          resolve(x);
        }
      } catch (e) {
        if (!called) {
          called = true;
          reject(e);
        }
      }
    } else {
      resolve(x);
    }
  }
}

参考资料

相关推荐
zhanghaisong_201525 分钟前
Caused by: org.attoparser.ParseException:
前端·javascript·html·thymeleaf
南城夏季1 小时前
蓝领招聘二期笔记
前端·javascript·笔记
Huazie1 小时前
来花个几分钟,轻松掌握 Hexo Diversity 主题配置内容
前端·javascript·hexo
NoloveisGod2 小时前
Vue的基础使用
前端·javascript·vue.js
GISer_Jing2 小时前
前端系统设计面试题(二)Javascript\Vue
前端·javascript·vue.js
海上彼尚2 小时前
实现3D热力图
前端·javascript·3d
理想不理想v2 小时前
使用JS实现文件流转换excel?
java·前端·javascript·css·vue.js·spring·面试
EasyNTS3 小时前
无插件H5播放器EasyPlayer.js网页web无插件播放器vue和react详细介绍
前端·javascript·vue.js
老码沉思录3 小时前
React Native 全栈开发实战班 - 数据管理与状态之Zustand应用
javascript·react native·react.js
poloma3 小时前
五千字长文搞清楚 Blob File ArrayBuffer TypedArray 到底是什么
前端·javascript·ecmascript 6