JS|JS 原生方法原理探究(十)(如何手写实现 Promise/A+ 及其方法())

这是 JS 原生方法原理探究系列的第十篇文章。本文会介绍如何手写一个符合 Promise A+ 规范的 Promise,并顺带实现 Promise 的相关方法。
实现 Promise/A+ 术语
为了更好地阅读本文,先约定一些术语和说法:
JS|JS 原生方法原理探究(十)(如何手写实现 Promise/A+ 及其方法())
文章图片

  • promise 初始的时候状态还没有落定,处于 pending 状态;它可以落定为 resolved 状态(fulfilled 状态),用 value 表示它 resolve 的值;也可以落定为 rejected 状态,用 reason(拒因)表示它 reject 的值。
  • then 方法接受的成功回调函数称为 onFulfilled,失败回调函数称为 onRejected
实现 Promise 构造函数
我们先尝试实现一个基础的 Promise 构造函数。
首先,用三个常量表示 promise 实例的状态:
const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected'

Promise 构造函数的作用是创建一个 promise 实例。对于一个 promise 实例来说,它会有几个基本的属性:status 记录 promise 的状态(初始为 pending),value 记录 promise resolve 的值(初始为 null),reason 记录 promise reject 的值(初始为 null)。
我们分别在 Promise 构造函数中进行定义:
function Promise(){ this.status = PENDING this.value = https://www.it610.com/article/null this.reason = null }

在 new 调用 Promise 构造函数的时候,会往构造函数中传入一个执行器函数 executor,这个执行器函数会马上执行,并且它本身接受 resovle 函数和 reject 函数作为参数。resolve 函数和 reject 函数负责实际改变 promise 的状态,它们的调用时机取决于开发者自己定义的执行器函数的逻辑,我们只需要编写调用执行器函数的代码即可。
所以代码进一步拓展如下:
function Promise(executor){ // 保存 promise 实例的引用,方便在 resolve 函数和 reject 函数中访问 let self = this self.status = PENDING self.value = https://www.it610.com/article/null self.reason = null // 定义 resolve 函数 function resolve(){ ... } // 定义 reject 函数 function reject(){ ... } // 调用执行器函数 executor(resolve,reject) }

resolve 函数和 reject 函数的作用是分别接受 value 和 reason 作为参数,并基于这两个值改变 promise 的状态,但为了确保 promise 状态的不可逆,必须在确定 promise 状态为 pending 的时候,才能修改其状态。所以 resolve 函数和 reject 函数定义如下:
function resolve(value){ if(self.status === PENDING){ self.status = FULFILLED self.value = https://www.it610.com/article/value } } function reject(reason){ if(self.status === PENDING){ self.status = REJECTED self.reason = reason } }

开发者会给 Promise 构造函数传入一个自定义的 executor,executor 中可能会调用 resolve 或者 reject,从而最终创建一个状态落定的 promise 实例。但根据规范的说法,executor 本身执行的时候可能是会抛出异常的,如果是这样,需要捕获异常并返回一个 reject 该异常的 promise 实例。所以修改代码如下:
function Promise(executor){ let self = thisself.status = PENDING self.value = https://www.it610.com/article/null self.reason = reasonfunction resolve(value){ if(self.status === PENDING){ self.status = FULFILLED self.value = value } } function reject(reason){ if(self.status === PENDING){ self.status = REJECTED self.reason = reason } } // 捕获调用 executor 的时候可能出现的异常 try{ executor(resolve,reject) } catch(e) { reject(e) } }

实现 promise 实例的 then 方法
1)初步实现 then 方法 所有的 promise 实例都可以调用 then 方法。then 方法负责对状态落定的 promise 作进一步的处理,它接受成功回调函数 onFulfilled 和失败回调函数 onRejected 作为参数,而 onFulfilled 和 onRejected 又分别接受 promise 的 value 和 reason 作为参数。
then 方法始终是同步执行的,根据执行 then 方法的时候 promise 状态的不同,会有不同的处理逻辑:
(1)如果 promise 是 resolved 状态,则执行 onFulfilled 函数
(2)如果 promise 是 rejected 状态,则执行 onRejected 函数
(3)如果 promise 是 pending 状态,则暂时不会执行 onFulfilled 函数和 onRejected 函数,而是先将这两个函数分别放到一个缓存数组中,等到将来 promise 状态落定的时候,再从数组中取出对应的回调函数执行
(注意:实际上,onFulfilled 和 onRejected 的执行是异步的,但目前我们暂时认为它们是同步执行)
无论是以上哪一种情况,调用 then 方法之后最终都会返回一个新的 promise 实例,这是 then 方法可以实现链式调用的一个关键。
按照上面的说法,初步实现的 then 方法如下:
Promise.prototype.then = function (onFulfilled,onRejected) { // 因为是 promise 实例调用 then 方法,所以 this 指向实例,这里保存以备后用 let self = this // 最终返回的 promise let promise2 // 1)如果是 fulfilled 状态 if(self.status === FULFILLED){ return promise2 = new Promise((resolve,reject) => { onFulfilled(self.value) }) } // 2)如果是 rejected 状态 else if(self.status === REJECTED){ return promise2 = new Promise((resolve,reject) => { onRejected(self.reason) }) } // 3)如果是 pending 状态 else if(self.status === PENDING){ return promise2 = new Promise((resolve,reject) => { self.onFulfilledCallbacks.push(() => { onFulfilled(self.value) }) self.onRejectedCallbacks.push(() => { onRejected(self.reason) }) }) } }

2)修改 Promise 构造函数:新增两个缓存数组 可以看到,这里多了两个缓存数组:onFulfilledCallbacksonRejectedCallbacks,它们实际上也是挂载到 promise 实例上的,因此修改一下 Promise 构造函数:
function Promise(executor){ // ...省略其它代码... // 新增两个缓存数组 self.onFulfilledCallbacks = [] self.onRejectedCallbacks = [] }

3)修改 resolve 和 reject 函数:执行缓存数组中的回调函数 因为执行 then 方法的时候,前面 promise 的状态还没有落定,我们并不知道应该执行哪个回调函数,因此选择把成功回调和失败回调先存入缓存数组中。那么什么时候应该执行回调函数呢?必然是 promise 状态落定的时候,又由于 promise 状态的落定依靠的是 resolve 函数和 reject 函数,因此这两个函数执行的时机,正是缓存数组中的回调函数执行的时机。
修改一下 resolve 函数和 reject 函数:
function resolve(value){ if(self.status === PENDING){ self.status = FULFILLED self.value = https://www.it610.com/article/value // 遍历缓存数组,取出所有成功回调函数执行 self.onFulfilledCallbacks.forEach(fn => fn()) } } function reject(reason){ if(self.status === PENDING){ self.status = REJECTED self.reason = reason // 遍历缓存数组,取出所有成功回调函数执行 self.onRejectedCallbacks.forEach(fn => fn()) } }

4)改进 then 方法:异常捕获 根据规范的说法,在执行成功回调或者失败回调的时候,回调本身可能抛出异常,如果是这样,则需要捕获该异常,并且最终返回一个 reject 该异常的 promise 实例。
因此在所有执行回调的地方包裹上 try...catch,改进 then 方法如下:
Promise.prototype.then = function (onFulfilled,onRejected) { // 因为是 promise 实例调用 then 方法,所以 this 指向实例,这里保存以备后用 let self = this // 最终返回的 promise let promise2 if(self.status === FULFILLED){ return promise2 = new Promise((resolve,reject) => { try { onFulfilled(self.value) } catch (e) { reject(e) } }) } else if(self.status === REJECTED){ return promise2 = new Promise((resolve,reject) => { try { onRejected(self.reason) } catch (e) { reject(e) } }) } else if(self.status === PENDING){ return promise2 = new Promise((resolve,reject) => { self.onFulfilledCallbacks.push(() => { try { onFulfilled(self.value) } catch (e) { reject(e) } }) self.onRejectedCallbacks.push(() => { try { onRejected(self.reason) } catch (e) { reject(e) } }) }) } }

5)改进 then 方法:实现值的穿透 有时候,可能不会给 then 方法传函数类型的参数,或者根本没有传参数,比如:
// 传入非函数类型的参数 Promise.reject(1).then(null,{}).then(null,err => { console.log(err)// 依然正常打印 1 })// 没有传参数 Promise.resolve(1).then().then(res => { console.log(res)// 依然正常打印 1 })

但即便如此,初始 promise 的 value 或者 reason 依然可以穿透 then 方法,往下传递,这就是 promise 值穿透的特性。要实现这个特性,实际上可以先判断传给 then 方法的参数是不是函数,如果不是(包含没有传参的情况),那么就自定义一个回调函数:
  • onFulfilled 如果不是函数:定义一个返回 value 的函数,将 value 往下传递,由后面的成功回调捕获
  • onRejected 如果不是函数:定义一个抛出 reason 的函数,将 reason 往下传递,由后面的失败回调捕获
因此改进 then 方法如下:
Promise.prototype.then = function (onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason } // ...省略其它代码... }

6)改进 then 方法:确定 then 方法的返回值 目前为止,我们还没有实现最关键的逻辑,也就是确定 then 方法的返回值 —— 虽然前面的代码已经让 then 方法返回了一个 promise,但是我们并没有确定这个 promise 的状态。
调用 then 之后返回的 promise 的状态,取决于回调函数的返回值,这部分的逻辑比较复杂,我们会用一个 resolvePromise 函数单独进行处理,而 then 方法内部只负责调用这个方法。
改进 then 方法如下:
Promise.prototype.then = function (onFulfilled,onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }let self = this// 最终返回的 promise let promise2 if(self.status === FULFILLED){ return promise2 = new Promise((resolve,reject) => { try { let x = onFulfilled(self.value) // 用 resolvePromise 处理 then 的返回值 resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) } else if(self.status === REJECTED){ return promise2 = new Promise((resolve,reject) => { try { let x = onRejected(self.reason) resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) } else if(self.status === PENDING){ return promise2 = new Promise((resolve,reject) => { self.onFulfilledCallbacks.push(() => { try { let x = onFulfilled(self.value) resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) self.onRejectedCallbacks.push(() => { try { let x = onRejected(self.reason) resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) }) } }

实现 resolvePromise 方法
始终要记住 resolvePromise 的目标是 基于回调函数返回值确定调用 then 之后返回的 promise 的状态,因此 resolvePromise 中的 resolve 调用或者 reject 调用将会决定最终返回的 promise 的状态
1)大致思路 实现 resolvePromise 方法的大致思路如下:
  1. 首先判断回调函数的返回值 x 是否等于调用 then 之后的返回值 promise2,如果相等,则直接返回一个 reject,拒因(reason)是一个 TypeError。这是因为,promise2 的状态取决于 x,如果两者是同一个对象,说明它需要自己决定自己的状态,这是做不到的。
    // 这样是会报错的,因为 then 的返回值等于回调函数的返回值 let p = Promise.resolve(1).then(res => p)

  2. 接着判断 x 是不是一个非 null 对象或者函数:
    1. 如果不是:则 x 绝不可能是一个 thenable,此时直接 resolve x 即可
    2. 如果是,再判断 x.then 是不是一个函数:
      1. 如果是:则 x 是一个 thenable,往后继续处理
      2. 如果不是:则 x 是一个非 thenable 的对象或函数,直接 resolve x 即可
按照这个思路实现的代码如下:
function resolvePromise(promise2,x,resolve,reject){ // 如果 promise2 和 x 是同一个对象,则会导致死循环 if(promise2 === x){ return reject(new TypeError('Chaining cycle')) } // 如果 x 是对象或者函数 if(x !== null && typeof x === 'object' || typeof x === 'function'){ // 如果 x 是一个 thenable if(typeof x.then === 'function'){ //...继续处理... } // 否则 else { resolve(x) } } // 否则 else { resolve(x) } }

2)如何处理 x 是 thenable 的情况 如果 x 是一个 thenable(包括 x 是 promise 的情况),应该怎么处理呢?先看一个例子:
let p1 = Promise.resolve(1).then(res => { return new Promise((resolve,reject) => { resolve(2) }) }) let p2 = Promise.resolve(1).then(res => { return new Promise((resolve,reject) => { reject(2) }) }) // 打印 p1 和 p2 的结果,分别是: Promise 2 Promise 2

可以看到,回调函数的返回值 x 是一个 thenable 的时候,调用 then 之后返回的 promise 会沿用 x 的 value 或者 reason。
因此我们要做的事情其实很简单,那就是在判断 x 是一个 thenable 之后,马上调用它的 then 方法,并且传入 resolve 和 reject 作为成功回调和失败回调。不管 x 的状态是否落定,它总会在某一个时刻基于自己的状态去调用 resolve 或者 reject,而且也会传入 x 的 value 或者 reason,这样就相当于我们调用了 resolve(value) 或者 reject(reason),因此得以确定调用 then 之后返回的 promise 的状态。
但是这里有一个问题,考虑下面的代码:
let p1 = Promise.resolve(1).then(res => { return new Promise((resolve,reject) => { resolve(new Promise((resolve,reject) => { resolve(2) })) }) }) let p2 = Promise.resolve(1).then(res => { return new Promise((resolve,reject) => { reject(new Promise((resolve,reject) => { resolve(2) })) }) }) // 打印 p1 和 p2 的结果,分别是: Promise { : 2} Promise { : Promise }

这里的区别在于,虽然回调函数也是返回一个 promise,但是这个 promise 内部 resolve 的还是一个 promise。如果按照先前的说法直接调用 resolve(value),则最终返回的 promise 是一个 resolve promise 的 promise,但实际上,它应该是一个 resolve 最里层 value(本例是 2)的 promise。所以,这里不能直接使用 resolve(value),而应该递归调用 resolvePromise(promise2,value,resolve,reject),直到找到最里层的基础值作为最终 resolve 的值。
不过,如果回调函数返回的 promise 内部 reject 的还是一个 promise,则最终返回的 promise 也是一个 reject promise 的 promise,这种情况并不需要递归调用找到最里层的基础值。
因此,这部分的代码如下:
function resolvePromise(promise2,x,resolve,reject){ if(promise2 === x){ return reject(new TypeError('Chaining cycle')) } if(x !== null && typeof x === 'object' || typeof x === 'function'){ if(typeof x.then === 'function'){ x.then( (y) => { resolvePromise(promise2,y,resolve,reject) }, (r) => { reject(r) }) }else { resolve(x) } }else { resolve(x) } }

3)其它需要注意的要点 参照规范可以发现,我们的 resolvePromise 函数还有不少需要改进的地方:
1)Promise 有很多不同版本的实现,它们的具体行为可能会有差异。为了保证不同版本的 Promise 实现可以互操作,提高兼容性,在 resolvePromise 方法中会处理一些比较特殊的情况。包括:
  • x 的 then 属性可能通过 Object.defineProperty 定义了一个 getter,并且每次 get 的时候会抛出异常。因此一开始需要先尝试获取 x.then ,并捕获可能出现的异常 —— 一旦捕获到,就 reject 该异常(这代表最终返回的是一个 reject 该异常的 promise)
  • 在调用 then 的时候,不会通过 x.then 调用,而是通过 then.call(x) 调用。这是为什么呢?首先,我们已经在前面通过 let then = x.then 拿到 then 方法的引用了,所以这里考虑的是不要再重复去获取,而是直接使用 then 变量,但直接使用又会导致它丢失 this 指向,所以需要用 call 绑定 this 为 x。
2)传给 then 的成功回调和失败回调可能会执行多次,如果是这样,应该以最先执行的回调为准,其它的执行会被忽略。因此会用变量 called 标志某个回调是否已被执行
3)调用 then 的时候可能也会抛出异常,如果是这样,也要 reject 这个异常。但如果捕获异常的时候已经调用了成功回调或者失败回调,则不需要再 reject 了。
根据上面提到的要点进行改进,最后的 resolvePromise 函数如下:
function resolvePromise (promise2,x,resolve,reject) { if(promise2 === x){ return reject(new TypeError('Chaining cycle')) } if(x !== null && typeof x === 'object' || typeof x === 'function'){ let then let called = false try { then = x.then } catch (e) { return reject(e) } if (typeof then === 'function') { try { then.call(x,(y) => { if(called) return called = true resolvePromise(promise2,y,resolve,reject) },(r) => { if(called) return called = true reject(r) }) } catch (e) { if(called) return reject(e) } } else { resolve(x) } } else { resolve(x) } }

实现回调函数的异步执行
最后,还需要注意的是 then 的回调函数的执行时机。
如果只看前面代码的实现,会认为在 promise 状态落定的情况下,执行 then 就会同步执行里面的回调,但实际上并非如此 —— then 里面的回调是异步执行的。这点规范也有提到:
In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously.
具体地说,执行 then 的时候:
  • 如果前面的 promise 状态落定:那么会先把 then 的回调推入任务队列,等同步代码执行完毕再从队列中取出回调执行。
  • 如果前面的 promise 状态未落定:那么会先把 then 的回调存入对应的缓存数组中,等 promise 的状态落定后,再从对应的数组中取出回调,推入任务队列中,等同步代码执行完毕再从队列中取出回调执行。
那么问题来了,回调函数的执行是属于微任务还是宏任务呢?
可以看一下规范的说法:
This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick.
说得很清楚了,A+ 规范只是明确了回调函数必须是异步执行的,并没有要求它必须是微任务或者宏任务。也就是说,依赖宏任务去实现 Promise 也是没有问题的,理论上也可以通过 A+ 测试。真正要求 Promise 必须依赖微任务去实现的是 HTML 标准,这在相关的文档中也可以查得到。
所以,如果要模拟回调函数的异步执行,也有两种方式。第一种就是基于宏任务去实现,用 setTimeout 包裹回调函数的执行;第二种则是基于微任务去实现,可以考虑使用 queueMicrotask 或者 process.nextTick
1)基于宏任务的实现 回调函数的执行逻辑是在 then 方法中编写的,因此只需要修改 then 方法,在原先执行回调函数的逻辑外面包裹上一个 setTimeout 即可:
Promise.prototype.then = function (onFulfilled,onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }let self = this// 最终返回的 promise let promise2 if(self.status === FULFILLED){ return promise2 = new Promise((resolve,reject) => { setTimeout(() => { try { let x = onFulfilled(self.value) // 用 resolvePromise 处理 then 的返回值 resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) }) } else if(self.status === REJECTED){ return promise2 = new Promise((resolve,reject) => { setTimeout(() => { try { let x = onRejected(self.reason) resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) }) } else if(self.status === PENDING){ return promise2 = new Promise((resolve,reject) => { self.onFulfilledCallbacks.push(() => { setTimeout(() => { try { let x = onFulfilled(self.value) resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) }) self.onRejectedCallbacks.push(() => { setTimeout(() => { try { let x = onRejected(self.reason) resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) }) }) } }

这样,调用 setTimeout 的时候,只会把传给它的回调函数放入宏任务队列,可以认为就是把成功回调或者失败回调放入宏任务队列。
2)基于微任务的实现 同样的,如果想要基于微任务去实现 Promise,可以用 queueMicrotask 去包裹回调函数的执行,这样可以将其执行放到一个微任务队列中。
Promise.prototype.then = function (onFulfilled,onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }let self = this// 最终返回的 promise let promise2 if(self.status === FULFILLED){ return promise2 = new Promise((resolve,reject) => { queueMicrotask(() => { try { let x = onFulfilled(self.value) // 用 resolvePromise 处理 then 的返回值 resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) }) } else if(self.status === REJECTED){ return promise2 = new Promise((resolve,reject) => { queueMicrotask(() => { try { let x = onRejected(self.reason) resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) }) } else if(self.status === PENDING){ return promise2 = new Promise((resolve,reject) => { self.onFulfilledCallbacks.push(() => { queueMicrotask(() => { try { let x = onFulfilled(self.value) resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) }) self.onRejectedCallbacks.push(() => { queueMicrotask(() => { try { let x = onRejected(self.reason) resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) }) }) } }

在 Node 环境下,还可以使用 process.nextTick() 代替 queueMicrotask
PS:另外需要注意的是,Node v11 之后才引入了 queueMicrotask 方法,因此要注意升级 Node 的版本,否则会无法通过 A+ 测试。
完善 resolve 函数
实际上,我们当前的代码已经可以通过 A+ 测试了,不过,我们的 resolve 函数还有需要完善的地方。
先看 reject 函数。new Promise 创建实例的时候,如果 reject 函数接受的参数也是一个 promise,那么最终返回的 实例会是怎么样的呢?使用原生 Promise 试一下:
let p1 = new Promise((resolve,reject) => { reject(new Promise((resolve,reject) => { resolve(123) })) }) let p2 = new Promise((resolve,reject) => { reject(new Promise((resolve,reject) => { reject(123) })) }) // 打印 Promise {fulfilled: 123} p1.then(null,e => { console.log(e) }) // 打印 Promise {rejected: 123} p2.then(null,e => { console.log(e) })

可以看到,即使 reject 函数接受的参数是一个 promise,它也会以这一整个 promise 作为 reason,返回一个 rejected 状态的 promise。而我们前面实现的 reject 函数的逻辑也正是这样的,这说明这个函数的实现没有问题。
但 resolve 函数就不一样了。使用原生 Promise 试一下:
let p1 = new Promise((resolve,reject) => { resolve(new Promise((resolve,reject) => { resolve(123) })) }) let p2 = new Promise((resolve,reject) => { resolve(new Promise((resolve,reject) => { reject(123) })) }) // 打印 value 123 p1.then( (value) => {console.log('value',value)}, (reason) => {console.log('reason',reason)} ) // 打印 reason 123 p2.then( (value) => {console.log('value',value)}, (reason) => {console.log('reason',reason)} )

可以看到,如果给 resolve 函数传入的是 resolved 状态的 promise(这里嵌套多少层 resolved 状态的 promise 都一样),则最终会返回一个 resolve 最里层 value 的 promise;如果传入的是 rejected 状态的 promise,则最终会返回一个和它“一样的” promise(状态一样,reason 也一样)。
但是按照我们前面实现的 resolve 函数的逻辑,我们统一将传给 resolve 的参数作为 value,并始终返回一个 resolved 状态的 promise。很明显,这和原生的行为是不符合的(注意,没有说这是错误的,因为 A+ 规范对这一点并没有提出要求)。那么应该怎么修改呢?
其实也很简单,那就是检测传给 resolve 的参数是不是 promise,如果是的话,就通过这个参数继续调用 then 方法。这样,如果参数是 rejected 状态的 promise,则调用 then 意味着调用失败回调函数 reject,并传入参数的 reason,从而确保最终返回的是一个和参数状态相同、reason 也相同的 promise;而如果参数是 resolved 状态的 promise,则调用 then 意味着调用成功回调函数 resolve,并传入参数的 value,从而确保最终返回的是一个和参数状态相同、value 也相同的 promise —— 即使存在多个 resolved 状态的 promise 的嵌套也没关系,反正我们最后总可以拿到最里层 resolve 的值。
所以,修改后的 resolve 函数如下:
function resolve(value){ if (value instanceof Promise) { return value.then(resolve,reject) } if(self.status === PENDING){ self.status = FULFILLED self.value = https://www.it610.com/article/value self.onFulfilledCallbacks.forEach(fn => fn()) } }

最终的代码
最终实现的代码如下:
// promise.jsconst PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected'function Promise (executor) { let self = this self.status = PENDING self.value = null self.reason = null self.onFulfilledCallbacks = [] self.onRejectedCallbacks = [] function resolve(value){ if (value instanceof Promise) { return value.then(resolve,reject) } if(self.status === PENDING){ self.status = FULFILLED self.value = https://www.it610.com/article/value self.onFulfilledCallbacks.forEach(fn => fn()) } } function reject(reason){ if(self.status === PENDING){ self.status = REJECTED self.reason = reason self.onRejectedCallbacks.forEach(fn => fn()) } } try { executor(resolve,reject) } catch (e) { reject(e) } } Promise.prototype.then = function (onFulfilled,onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e } let self = this let promise2 if (self.status === FULFILLED) { return promise2 = new Promise((resolve,reject) => { queueMicrotask(() => { try { let x = onFulfilled(self.value) resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) }) } else if (self.status === REJECTED) { return promise2 = new Promise((resolve,reject) => { queueMicrotask(() => { try { let x = onRejected(self.reason) resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) }) } else if (self.status === PENDING) { return promise2 = new Promise((resolve,reject) => { self.onFulfilledCallbacks.push(() => { queueMicrotask(() => { try { let x = onFulfilled(self.value) resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) }) self.onRejectedCallbacks.push(() => { queueMicrotask(() => { try { let x = onRejected(self.reason) resolvePromise(promise2,x,resolve,reject) } catch (e) { reject(e) } }) }) }) } } function resolvePromise (promise2,x,resolve,reject) { if(promise2 === x){ return reject(new TypeError('Chaining cycle!')) } if (x !== null && typeof x === 'object' || typeof x === 'function') { let then try { then = x.then } catch (e) { reject(x) } if (typeof then === 'function') { let call = false try { then.call(x,(y) => { if(called) return called = true resolvePromise(promise2,y,resolve,reject) },(r) => { if(called) return called = true reject(r) }) } catch (e) { if(called) return reject(e) } } else { resolve(x) } } else { resolve(x) } }

Promise A+ 测试
可以借助 promises-aplus-test 这个库对我们实现的 Promise 进行测试。
先通过 npm 安装:
npm install promises-aplus-test -D

接着在 promise.js 文件中添加:
// promise.jsPromise.defer = Promise.deferred = function () { let dfd = {}; dfd.promise = new Promise((resolve, reject) => { dfd.resolve = resolve; dfd.reject = reject; }); return dfd; }module.exports = Promise;

最后运行测试:
promises-aplus-test ./promise.js

测试结果如下:
JS|JS 原生方法原理探究(十)(如何手写实现 Promise/A+ 及其方法())
文章图片

成功通过 872 个测试用例,说明我们实现的 Promise 是符合 A+ 规范的。如果某些测试用例没有通过,可以再对照规范改一改。
实现 Promise 的静态方法和原型方法 Promise A+ 规范并没有对 Promise 的静态方法和原型方法(除了 then 方法)的实现提出要求,但是有了前面的基础,实现这些方法也并不难。下面我们一个一个来实现。
Promise.resolve()
Promise.resolve 接受的参数如果是一个 promise,则最终将这个 promise 原样返回;如果是一个 thenable,则返回一个采用该 thenable 状态的 promise;其它情况下,一律返回 resolve 给定参数的 promise。
实现如下:
Promise.resolve = (param) => { if(param instanceof Promise){ return param } return new Promise((resolve,reject) => { // 如果是 thenable if(param && param.then && typeof param.then === 'function'){ param.then(resolve,reject) } else { resolve(param) } }) }

PS:为什么调用 param.then(resolve,reject) 可以让返回的 promise 沿用 param 的状态呢?因为 param 总会在某一个时刻执行 then 里面的某个回调,并且传入对应的参数 —— 也就是执行 resolve(value) 或者 reject(reason),而这个执行是在返回的 promise 内部进行的,所以返回的 promise 一定会沿用 param 的状态。
Promise.reject()
任何情况下,Promise.reject() 都会返回一个 reject 给定参数的 promise:
Promise.reject = (param) => { return new Promise((resolve,reject) => { reject(param) }) }

Promise.all()
Promise.all() 接受的参数:
  • 不可迭代时,返回一个 rejected 状态的 promise;
  • 可迭代时,如果是空的可迭代对象,则返回一个 resolve 空数组的 promise;
  • 可迭代时,如果是非空的可迭代对象:
    • 不包含 rejected 状态和 pending 状态的 promise,则返回一个 resolve 结果数组的 promise,结果数组中包含各个 promise 的 resolved 值
    • 包含一个 rejected 状态的 promise,返回一个相同的 promise
    • 不包含 rejected 状态的 promise,但包含 pending 状态的 promise,则返回一个 pending 状态的 promise
PS:可迭代对象中的每个成员都会被 Promise.resolve() 包装成一个 promise
因此实现的代码如下:
Promise.all = (promises) => { // 判断是否可迭代 let isIterable = (params) => typeof params[Symbol.iterator] === 'function' return new Promise((resolve,reject) => { // 如果不可迭代 if(!isIterable(promises)) { reject(new TypeError(`${promises} is not iterable!`)) }else { let result = [] let count = 0 if(promises.length === 0){ resolve(result) } else { for(let i = 0; i < promises.length; i++){ Promise.resolve(promises[i]).then((value) => { count++ result[i] = value if(count === promises.length); resolve(result) },reject) } } } }) }

可以看到,我们会遍历 promises 中的每一个成员,用 count 记录 resolved 状态的 promise 的个数,并把它们的 value 存入结果数组中,只要发现所有成员都是 resolved 状态的 promise,就会返回一个 resolve 结果数组的 promise;而只要发现有一个 rejected 状态的 promise,就会以它的 reason 作为 reason,返回一个 rejected 状态的 promise;如果存在 pending 状态的 promise,则必不可能执行 resolve 或者 reject,因此最后会返回一个同样是 pending 状态的 promise。
Promise.race()
Promise.all() 类似,但 Promise.race() 只要求有一个 promise 状态落定即可,并且最终会返回一个和它一样的 promise。如果传入的是一个空的可迭代对象,则意味着它永远无法得到一个期望的状态落定的 promise,但是,它还是会继续等待下去,因此最终会返回一个 pending 状态的 promise。
实现代码如下:
Promise.race = (promises) => { let isIterable = (param) => typeof param[Symbol.iterator] === 'function' return new Promise((resolve,reject) => { if (!isIterable(promises)) { reject(new TypeError(`${promises} is not iterable!`)) } else { for(let i = 0; i < promises.length; i++){ Promise.resolve(promises[i]).then(resolve,reject) } } }) }

其实大体上来说只有两种情况:
  • 一种是 promises 中至少有一个状态落定的 promise,那么遇到这个 promise 的时候,就会通过它去调用 then,进而调用 resolve 或者 reject,使最终返回的 promise 状态落定。即使此后又遇到了其它状态落定的 promise 并且执行相应的 resolve 或者 reject,也没有关系,因为 promise 的状态是不可逆的
  • 另一种是所有 promise 的状态都未落定,这意味着永远不可能执行 then 里面的回调,也即不可能执行 resolve 或者 reject,因此最终返回的是一个 pending 状态的 promise
Promise.allSettled()
Promise.all() 类似,也会返回一个 resolve 结果数组的 promise,但是结果数组中会包含各个 promise 的 resolved 值或者 rejected 值,类似于这样:
[ {status: "fulfilled", value: 11} {status: "rejected", reason: 22} {status: "fulfilled", value: 33} ]

如果 promises 中存在 pending 状态的 promise,则无法达到真正的 “allSettled”(全部落定),最终会返回一个 pending 状态的 promise。
实现代码如下:
Promise.allSettled = (promises) => { let isIterable = param => typeof param[Symbol.iterator] === 'function' return new Promise((resolve,reject) => { if (!isIterable(promises)) { reject(new TypeError(`${promises} is not iterable!`)) } else { let result = [] let count = 0 if(promises.length === 0) { resolve(result) } else { for(let i = 0; i < promises.length; i++){ Promise.resolve(promises[i]).then( value => { count++ result[i] = { status: 'fulfilled', value } if(count === promises.length) resolve(result) }, reason => { count++ result[i] = { status: 'rejected', reason } if(count === promises.length) resolve(result) } ) } } } }) }

Promise.prototype.catch()
如果前面的 promise 落定为 rejected 状态,则会执行 catch 方法,因此 catch 方法可以看作是没有传入成功回调作为参数的 then 方法:
Promise.prototype.catch = (onRejected) => { return this.then(null,onRejected) }

Promise.prototype.finally()
finally 方法的特点有两个:
  • 不管前面的 promise 是 resolved 状态还是 rejected 状态,传给 finally 的回调函数都可以执行
  • finally 最后也会返回一个 promise,这个 promise 一般会沿用调用 finally 的 promise 的状态。除非 finally 的回调函数返回了一个 rejected 状态的 promise
最终的实现如下:
Promise.prototype.finally = (fn) => { let P = this.constructor return this.then( value=> P.resolve(fn()).then(() => value), reason => P.resolve(fn()).then(() => { throw reason }) ) }

注意几个要点:
1)这里不直接使用 Promise.resolve(),是因为如果这样写,那么这个 finally 方法就只能兼容我们的 Promise 版本了;而通过 promise 实例的 constructor 则始终可以获取该实例对应的 Promise 版本
2)因为调用 finally 后返回的 promise 的状态依赖于调用 finally 的 promise 实例,所以返回一个 this.then(...) ,方便获取 promise 实例的 value 或者 reason
3)在 then 的成功回调和失败回调中,不仅执行了 fn,而且还将其执行结果用 P.resolve() 包装起来,这主要是为了处理 fn 执行结果可能为 promise 的情况 —— 在这种情况下,它可能会影响到最后返回的 promise 的状态。
通过两个例子来理解这样写的目的。比如说:
Promise.resolve(1).finally(() => {return Promise.resolve(2)})

按照我们的代码,调用 finally 之后将会返回 Promise.resolve(1).then(...),走成功回调的逻辑,获取的 value 就是 1。而 P.resolve(fn()) 将会返回 fn(),也就是 Promise.resolve(2),走成功回调的逻辑,回调返回 value。最终调用 finally 返回的就恰好是一个 resolve 1 的 promise。
但如果是:
Promise.resolve(1).finally(() => {return Promise.reject(2)})

【JS|JS 原生方法原理探究(十)(如何手写实现 Promise/A+ 及其方法())】注意这里回调返回的虽然也是 promise,但它是 rejected 状态的。那么调用 finally 之后将会返回 Promise.resolve(1).then(...),走成功回调的逻辑,获取的 value 就是 1。而 P.resolve(fn()) 将会返回 fn(),也就是 Promise.reject(2),走失败回调的逻辑,注意这里我们没有声明失败回调,所以会采用默认的失败回调, 接受前面 promise reject 掉的 2,并将这个 2 抛出去,所以最终调用 finally 返回的就恰好是一个 reject 2 的 promise。这种情况下,最终的 promise 并没有沿用调用 finally 的 promise 的状态,而是依赖于 finally 回调的执行结果。

    推荐阅读