一步一步手写 Promise
根据 Promises/A+ 规范实现 Promise
Promise/A+ 规范
Promises/A+ 规范(以下简称规范)可以在 Promises/A+ 查看,需要中文可以选择 AI 翻译或者 Promise/A+ 规范
简单来说,符合 Promises/A+ 规范的 promise(以下简称 promise )表示异步操作的最终结果,其应该是一个具有 then
方法的对象或者函数,该 promise 的 then 方法的行为符合规范。promise 必须处于 pending
、fulfilled
、rejected
三种状态之一,且其状态只允许由 pending
转换为其他两种状态——即其他两种状态不允许改变。当处于 fulfilled
状态时,promise 具有一个不可修改的值,表示异步处理的结果值;当处于 rejected
状态时,promise 具有一个不可修改的拒因(reason),表示异步处理被拒绝(或者说失败)的原因
构造
Promise 类基本骨架 增加状态、最终值和拒因 回调方法 实现修改 promise 状态的方法 按照 ES6 中使用 Promise 的例子,此处将 Promise 定义为一个类
1 2 3 4 class MyPromise { constructor ( ) {} then ( ) {} }
根据规范,Promise 拥有三种状态(语意上为待定 、已满足 、被拒绝 ),拥有异步处理的值和拒因,则构造函数应该写为
1 2 3 4 5 6 7 8 class MyPromise { constructor ( ) { this .status = 'pending' this .value = undefined this .reason = undefined } then ( ) {} }
在实际使用 Promise 时,会在构造函数里传入一个接收两个函数参数的回调方法:
第一个参数方法负责将 promise 置为 fulfilled
状态
第二个参数方法负责将 promise 置为 rejected
状态
于是改造为以下写法,其中假设 fulfillPromise
方法可以实现上述功能1、rejectPromise
方法可以实现上述功能2
1 2 3 4 5 6 7 8 9 10 11 12 13 class MyPromise { constructor (fn ) { this .status = 'pending' this .value = undefined this .reason = undefined fn ((value ) => { fulfillPromise (this , value) }, (reason ) => { rejectPromise (this , reason) }) } then ( ) {} }
前述的 fulfillPromise
方法和 rejectPromise
方法根据规范要求,只需要将处于 pending
状态的 promise 的状态修改,并赋予对应的值或拒因即可
考虑到实现这两个方法时需要频繁交互 promise 的状态,因此将 promise 的三种状态提出为一个映射,避免每次都手写字符串做匹配或赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const STATUS = { PENDING : 'pending' , FULFILLED : 'fulfilled' , REJECTED : 'rejected' } function fulfillPromise (promise, value ) { if (promise.status !== STATUS .PENDING ) return promise.status = STATUS .FULFILLED promise.value = value } function rejectPromise (promise, reason ) { if (promise.status !== STATUS .PENDING ) return promise.status = STATUS .REJECTED promise.reason = reason }
到此,简单的 Promise 构造方法实现完毕
then 方法
此处需要回顾一下规范中的 then
方法
then
方法接收两个参数:
1 promise2 = promise.then (onFulfilled, onRejected)
其中的 onFulfilled
和 onRejected
都是可选参数,并且当它们各自不为函数时,会被忽略。它们各自只能在 promise 状态发生相应变化
相应变化(指变为 fulfilled 时调用 onFulfilled 并传入 promise 的值;变为 rejected 时调用 onRejected 并传入 promise 的拒因) 后被调用,且只能被调用一次
then
方法可以被调用多次。当 promise 被满足时,按照原始调用顺序依次执行回调 onFulfilled
,当 promise 被拒绝时,按照原始调用顺序依次执行回调 onRejected
then
方法必须返回一个 promise
由以上第1点得知,onFulfilled
或 onRejected
调用时:需要判断是否为函数,不为函数则忽略——返回的新 promise 的值或拒因与当前 promise 相同 ;需要判断 promise 的状态,避免 promise 不为 pending
时状态发生改变
由以上第2点可以得知,promise 需要有两个回调队列,满足依次回调 onFulfilled
或者 onRejected
。而且在 fulfillPromise
和 rejectPromise
中应该依次调用相应的回调队列
由此得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 const isFunction = function (func ) { return Object .prototype .toString .call (func).toLocaleLowerCase () === '[object function]' } const runCbs = function (cbs, value ) { cbs.forEach ((cb ) => { cb (value) }) } function fulfillPromise (promise, value ) { runCbs (promise.fulfilledCallbacks , value) } function rejectPromise (promise, reason ) { runCbs (promise.rejectedCallbacks , reason) } class MyPromise { constructor (fn ) { this .fulfilledCallbacks = [] this .rejectedCallbacks = [] } then (onFulfilled, onRejected ) { const promise1 = this const promise2 = new MyPromise (() => {}) if (promise1.status === STATUS .FULFILLED ) { if (isFunction (onFulfilled)) { } else { fulfillPromise (promise2, promise1.value ) } } else if (promise1.status === STATUS .REJECTED ) { if (isFunction (onRejected)) { } else { rejectPromise (promise2, promise1.reason ) } } else { promise1.fulfilledCallbacks .push (() => { }) promise1.rejectedCallbacks .push (() => { }) } return promise2 } }
上述代码中的 todo: 异步调用后执行回调
意味着需要调用回调方法执行回调。规范中 2.2.4 提到,必须在执行上下文 堆栈仅包含平台代码时调用 onFulfilled
和 onRejected
,换句话说就是需要异步执行回调 。这里简单使用 setTimeout
模拟一下,基本框架为:
1 2 3 4 5 6 7 8 9 10 11 12 13 setTimeout (() => { try { const x = onFulfilled (promise1.value ) resolvePromise (promise2, x) } catch (error) { rejectPromise (promise2, error) } }, 0 )
这里的 resolvePromise
方法,表示根据回调方法的返回值,决议(resolve)一个 promise
如果说 then
方法是一个 promise 对象的核心,那么这个 resolvePromise
方法就是 then
方法的核心,下文会提到,此处先按下不表
于是将这个异步调用也抽象成一个 simulateAsyncCall
方法:
1 2 3 4 5 6 7 8 9 10 function simulateAsyncCall (promise, onCall, value ) { setTimeout (() => { try { const x = onCall (value) resolvePromise (promise, x) } catch (e) { rejectPromise (promise, e) } }, 0 ) }
然后补全 todo
处的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 then (onFulfilled, onRejected ) { const promise1 = this const promise2 = new MyPromise (() => {}) if (promise1.status === STATUS .FULFILLED ) { if (isFunction (onFulfilled)) { simulateAsyncCall (promise2, onFulfilled, promise1.value ) } else { fulfillPromise (promise2, promise1.value ) } } else if (promise1.status === STATUS .REJECTED ) { if (isFunction (onRejected)) { simulateAsyncCall (promise2, onRejected, promise1.reason ) } else { rejectPromise (promise2, promise1.reason ) } } else { onFulfilled = isFunction (onFulfilled) ? onFulfilled : (value ) => { return value } onRejected = isFunction (onRejected) ? onRejected : (err ) => { throw err } promise1.fulfilledCallbacks .push ((value ) => { simulateAsyncCall (promise2, onFulfilled, value) }) promise2.rejectedCallbacks .push ((reason ) => { simulateAsyncCall (promise2, onRejected, reason) }) } return promise2 }
此时的完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 const STATUS = Object .freeze ({ PENDING : 'pending' , FULFILLED : 'fulfilled' , REJECTED : 'rejected' }) const isFunction = function (func ) { return Object .prototype .toString .call (func).toLocaleLowerCase () === '[object function]' } const runCallbacks = function (cbs, value ) { cbs.forEach ((cb ) => cb (value)) } const fulfillPromise = function (promise, value ) { if (promise.status !== STATUS .PENDING ) return promise.value = value promise.status = STATUS .FULFILLED runCallbacks (promise.fulfilledCallbacks , value) } const rejectPromise = function (promise, reason ) { if (promise.status !== STATUS .PENDING ) return promise.status = STATUS .REJECTED promise.reason = reason runCallbacks (promise.rejectedCallbacks , reason) } const resolvePromise = function (promise, value ) {}const simulateAsyncCall = function (promise, func, value ) { setTimeout (() => { try { const x = func (value) resolvePromise (promise, x) } catch (e) { rejectPromise (promise, e) } }, 0 ) } class MyPromise { constructor (fn ) { this .status = STATUS .PENDING this .value = undefined this .reason = undefined this .fulfilledCallbacks = [] this .rejectedCallbacks = [] fn ( (value ) => { resolvePromise (this , value) }, (reason ) => { rejectPromise (this , reason) } ) } then (onFulfilled, onRejected ) { const promise1 = this const promise2 = new MyPromise (() => {}) if (promise1.status === STATUS .FULFILLED ) { if (isFunction (onFulfilled)) { simulateAsyncCall (promise2, onFulfilled, promise1.value ) } else { fulfillPromise (promise2, promise1.value ) } } else if (promise1.status === STATUS .REJECTED ) { if (isFunction (onRejected)) { simulateAsyncCall (promise2, onRejected, promise1.reason ) } else { rejectPromise (promise2, promise1.reason ) } } else { onFulfilled = isFunction (onFulfilled) ? onFulfilled : (value ) => { return value } onRejected = isFunction (onRejected) ? onRejected : (err ) => { throw err } promise1.fulfilledCallbacks .push ((value ) => { simulateAsyncCall (promise2, onFulfilled, value) }) promise1.rejectedCallbacks .push ((reason ) => { simulateAsyncCall (promise2, onRejected, reason) }) } return promise2 } }
resolve
:决议 promise
实现 resolve
之前,先要引入一个之前用不到的术语(Terminology)——thenable
thenable
是一个定义 then 方法的对象或函数
听起来很像 promise?没错,按照规范的说法,这个 thenable
对象至少有点像 promise(at least somewhat like a promise)
规范区分 promise 和 thenable 想来是为了兼容不规范的 promise 实现
规范用了大篇幅介绍决议过程。决议过程将 promise 和 一个 x 值作为输入,因此可以假设决议方法 resolvePromise
的定义为:
1 const resolvePromise = function (promise, x ) {}
决议时,执行以下逻辑:
如果 promise 和 x 是同一个值,则以一个 TypeError
拒绝 promise
如果 x 是另一个 promise,则使用 x 的状态决议 promise
如果 x 是一个对象或者函数,则尝试读取 x.then
若读到的不是方法则以 x 为值满足 promise
若是则以 x 为 this 调用它,传入与 promise 的 then
方法相似的两个回调函数,只不过这两个回调函数会调用 resolvePromise
和 rejectPromise
来处理 promise 的状态
调用 x.then
时如果多次调用 resolvePromise
或者 rejectPromise
则应该只执行第一次调用而忽略后续调用
如果 x 不是对象也不是函数,则以 x 为值满足 promise
决议中的任何异常均会导致 promise 被以该异常拒绝(前提是 promise 仍处于 pending 状态)
对于第2点,所谓的使用 x 的状态决议 promise, 即指 x 已满足则 promise 也被满足, x 被拒绝则 promise 也被拒绝,x 待定则 promise 也待定,直到 x 被满足或者被拒绝。这个过程是很符合直觉的
对于第3点中的 x.then
是一个方法的情况,其实按照规范描述写代码可能会更直接一些
由此可以得到代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 const isObject = function (obj ) { return Object .prototype .toString .call (obj).toLocaleLowerCase () === '[object object]' } const isPromise = function (promise ) { return promise instanceof MyPromise } const resolvePromise = function (promise, x ) { if (x === promise) { rejectPromise (promise, new TypeError ('resolving promise must not use the same promise' )) } else if (isPromise (x)) { if (x.status === STATUS .FULFILLED ) { fulfillPromise (promise, x.value ) } else if (x.status === STATUS .REJECTED ) { rejectPromise (promise, x.reason ) } else { x.then ( (v ) => { fulfillPromise (promise, v) }, (r ) => { rejectPromise (promise, r) } ) } } else if (isObject (x) || isFunction (x)) { let then let called = false try { then = x.then } catch (e) { rejectPromise (promise, e) return } if (isFunction (then)) { try { then.call ( x, (v ) => { if (!called) { called = true resolvePromise (promise, v) } }, (r ) => { if (!called) { called = true rejectPromise (promise, r) } } ) } catch (e) { if (!called) { called = true rejectPromise (promise, e) } } } else { fulfillPromise (promise, x) } } else { fulfillPromise (promise, x) } }
测试
Github 上的 Promises/A+ 除了提供了 Promises/A+ 规范以外,还提供了测试自定义的 Promise 的方法
全局安装命令:
1 npm install promises-aplus-tests -g
局部安装可以参见上述仓库,这里不再赘述
安装完成后,对自定义的 Promise 文件增加如下修改:
1 2 3 4 5 6 7 8 9 10 MyPromise .deferred = function ( ) { const deferred = {} deferred.promise = new MyPromise ((resolve, reject ) => { deferred.resolve = resolve deferred.reject = reject }) return deferred } module .exports = MyPromise
接着运行以下命令即可看到测试结果:
1 promises-aplus-tests promise.js
修正
可以看到有一些用例,没有通过。最终排查到的结果,是在构造 Promise 执行传入的同步方法时,调用了 fulfillPromise
而不是 resolvePromise
方法,改动如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // @other code class MyPromise { constructor(fn) { this.status = STATUS.PENDING this.value = undefined this.reason = undefined this.fulfilledCallbacks = [] this.rejectedCallbacks = [] fn( (value) => { - fulfillPromise(this, value) + resolvePromise(this, value) }, (reason) => { rejectPromise(this, reason) } ) } // @other code }
当控制台输出 872 passing 即表示所有用例通过,当前的自定义 Promise 完整实现了 Promises/A+ 规范
完善
isFunction
和 isObject
方法均使用了 Object.prototype.toString.call()
这样的判断类型的方式,由此可以再抽象出一个 isType
方法,并将 isFunction
和 isObject
做如下改造:
1 2 3 4 5 6 7 8 9 const isType = function (type ) { return function (obj ) { return Object .prototype .toString .call (obj).toLocaleLowerCase () === '[object ' + type + ']' } } const isObject = isType ('object' )const isFunction = isType ('function' )
最终的 promise.js 文件内容为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 const STATUS = Object .freeze ({ PENDING : 'pending' , FULFILLED : 'fulfilled' , REJECTED : 'rejected' }) const isType = function (type ) { return function (obj ) { return Object .prototype .toString .call (obj).toLocaleLowerCase () === '[object ' + type + ']' } } const isObject = isType ('object' )const isFunction = isType ('function' )const isPromise = function (promise ) { return promise instanceof MyPromise } const runCallbacks = function (cbs, value ) { cbs.forEach ((cb ) => cb (value)) } const fulfillPromise = function (promise, value ) { if (promise.status !== STATUS .PENDING ) return promise.value = value promise.status = STATUS .FULFILLED runCallbacks (promise.fulfilledCallbacks , value) } const rejectPromise = function (promise, reason ) { if (promise.status !== STATUS .PENDING ) return promise.status = STATUS .REJECTED promise.reason = reason runCallbacks (promise.rejectedCallbacks , reason) } const resolvePromise = function (promise, x ) { if (x === promise) { rejectPromise (promise, new TypeError ('resolving promise must not use the same promise' )) } else if (isPromise (x)) { if (x.status === STATUS .FULFILLED ) { fulfillPromise (promise, x.value ) } else if (x.status === STATUS .REJECTED ) { rejectPromise (promise, x.reason ) } else { x.then ( (v ) => { fulfillPromise (promise, v) }, (r ) => { rejectPromise (promise, r) } ) } } else if (isObject (x) || isFunction (x)) { let then let called = false try { then = x.then } catch (e) { rejectPromise (promise, e) return } if (isFunction (then)) { try { then.call ( x, (v ) => { if (!called) { called = true resolvePromise (promise, v) } }, (r ) => { if (!called) { called = true rejectPromise (promise, r) } } ) } catch (e) { if (!called) { called = true rejectPromise (promise, e) } } } else { fulfillPromise (promise, x) } } else { fulfillPromise (promise, x) } } const simulateAsyncCall = function (promise, func, value ) { setTimeout (() => { try { const x = func (value) resolvePromise (promise, x) } catch (e) { rejectPromise (promise, e) } }, 0 ) } class MyPromise { constructor (fn ) { this .status = STATUS .PENDING this .value = undefined this .reason = undefined this .fulfilledCallbacks = [] this .rejectedCallbacks = [] fn ( (value ) => { resolvePromise (this , value) }, (reason ) => { rejectPromise (this , reason) } ) } then (onFulfilled, onRejected ) { const promise1 = this const promise2 = new MyPromise (() => {}) if (promise1.status === STATUS .FULFILLED ) { if (isFunction (onFulfilled)) { simulateAsyncCall (promise2, onFulfilled, promise1.value ) } else { fulfillPromise (promise2, promise1.value ) } } else if (promise1.status === STATUS .REJECTED ) { if (isFunction (onRejected)) { simulateAsyncCall (promise2, onRejected, promise1.reason ) } else { rejectPromise (promise2, promise1.reason ) } } else { onFulfilled = isFunction (onFulfilled) ? onFulfilled : (value ) => { return value } onRejected = isFunction (onRejected) ? onRejected : (err ) => { throw err } promise1.fulfilledCallbacks .push ((value ) => { simulateAsyncCall (promise2, onFulfilled, value) }) promise1.rejectedCallbacks .push ((reason ) => { simulateAsyncCall (promise2, onRejected, reason) }) } return promise2 } } MyPromise .deferred = function ( ) { const deferred = {} deferred.promise = new MyPromise ((resolve, reject ) => { deferred.resolve = resolve deferred.reject = reject }) return deferred } module .exports = MyPromise