Promise 是一个对象,用作 deferred 计算(可能是异步计算)的最终结果的 placeholder(占位符)。换句话说就是,一个 promise 代表了“异步操作”的最终结果。之所以一定是个“异步操作”,这取决于 promise 本身的执行机制,在事件循环里它属于微任务的一种。
Promise/A+
Promises/A+ 是 JavaScript promise 的开放标准,该规范详细介绍了 then
方法的行为。
作为与 promise 交互的主要方式,then
方法注册了两个回调,分别接收 promise 的正常返回值和出现异常的原因。
A. 相关术语
promise
是一个对象或函数,且有一个 then
方法
thenable
是一个对象或函数,用来定义 then
方法
value
是 promise 成功状态时的值,可以是任何合法的 JavaScript 值(包括 undefined
, thenable
, promise)
reason
是 promise 失败状态时的值,表示 promise 被拒绝的原因
exception
是使用 throw
语句抛出的值
在 promise 正式成为语言标准之前,JavaScript 生态系统中存在过很多种不同的 promise 实现。尽管它们的内部表示有所差异,但所有的 promise-like objects 都实现了 thenable 接口,即 then()
方法。
promise 也是 thenable。为了与现有的 promise 实现互操作,语言通常允许用 thenable 代替 promise。比如:
Copy const aThenable = {
then (onFulfilled , onRejected) {
onFulfilled ({
// The thenable is fulfilled with another thenable
then (onFulfilled , onRejected) {
onFulfilled ( 42 )
}
})
}
}
Promise .resolve (aThenable) // A promise fulfilled with 42
B. 要求
1. 三种状态
一个 promise 必须是以下三种状态之一:pending、fulfilled、rejected。且状态转移只能有两种:pending -> fulfilled、pending -> rejected。可以这样理解,promise 就是(承诺)其状态一旦改变,就永不可逆。
Copy // 以下语句可以从 pending -> rejected
reject ( "oh, no!" )
// reject(new Error("oh, no!"))
// throw ('oh, no!')
throw new Error ( 'oh, no!' ) // 注意:异步 throw,就和 uncaught error 一样
return Promise .reject ( 'oh, no!' )
// return Promise.reject(new Error('oh, no!'))
2. then()
方法
一个 promise 必须提供一个 then
方法,用来访问 promise 的当前 value
或 reason
它接收两个参数(都是可选的):
onFulfilled
方法表示状态从 pending -> fulfilled 时要执行的方法
onRejected
方法表示状态从 pending -> rejected 时要执行的方法
它必须返回一个 promise,为了实现 then()
的链式调用
Copy promise2 = promise1 .then (onFulfilled , onRejected)
接下来,就按照规范的相关要求,一步一步实现自己的 Promise。
1. 支持构造函数和 then()
1.1 目标
第一版的目标,是支持以下用法:
Copy // 需支持 new MyPromise()
let p = new MyPromise ((resolve , reject) => {
console .log ( 'init MyPromise' )
resolve ( 1 )
})
// 需支持 then() 方法
p .then (x => console .log ( `x = ${ x } ` ))
1.2 代码实现
Copy function MyPromise (constructor) {
this .state = 'pending' // 当前状态
this .value = undefined // 状态从 pending -> fulfilled(成功)时的值
this .reason = undefined // 状态从 pending -> rejected(失败)时的值
const resolve = (value) => {
// 状态从 pending -> fulfilled,不可逆
if ( this .state === 'pending' ) {
this .state = 'fulfilled'
this .value = value
}
}
const reject = (reason) => {
// 状态从 pending -> rejected,不可逆
if ( this .state === 'pending' ) {
this .state = 'rejected'
this .reason = reason
}
}
// 捕获构造函数的异常,因为 constructor 是用户“自定义”传进来的
try {
constructor (resolve , reject)
} catch (e) {
reject (e)
}
}
MyPromise . prototype . then = function (onFulfilled , onRejected) {
// 根据不同的状态,执行对应的方法
switch ( this .state) {
case 'fulfilled' :
// onFulfilled() 的第一个参数是 promise 的 value,且该方法只能调用一次
onFulfilled ( this .value)
break ;
case 'rejected' :
// onRejected() 的第一个参数是 promise 的 reason,且该方法只能调用一次
onRejected ( this .reason)
break ;
}
}
此时,运行目标代码,会成功输出:
另外,根据 Promises/A+ 规范的相关要求,还需要再完善下 then
方法,修改后的代码如下:
Copy MyPromise . prototype . then = function (onFulfilled , onRejected) {
//【标准 2.2.1】两个参数都是可选的,如果不是 function 则必须忽略
// 在实践中,会给它们赋个默认值,用来实现 `then` 方法的值穿透
// 也就是说,即便当前的 `then` 方法没传任何参数,promise 也能把值透传给下一个 `then` 方法
onFulfilled = ( typeof onFulfilled === 'function' ) ? onFulfilled : function (x) { return x };
onRejected = ( typeof onRejected === 'function' ) ? onRejected : function (e) { throw e };
//【标准 2.2.4】确保 onFulfilled() 和 onRejected() 异步执行,
// 要在 `then` 所在的事件循环之后调用,得用个新堆栈
// 在实践中,可以用宏任务机制(setTimeout, setImmediate)
// 或微任务机制(MutationObserver, process.nextTick)
// 考虑到 promise 本就属于微任务,所以更应该用“微任务”
// 简单起见,这里就用 setTimeout 示意下
switch ( this .state) {
case 'fulfilled' :
setTimeout (() => {
onFulfilled ( this .value)
})
break ;
case 'rejected' :
setTimeout (() => {
onRejected ( this .reason)
})
break ;
}
}
此时,运行目标代码,也可以正常输出。
1.3 实现思路
构造函数 constructor
由使用者自己提供
promise 的状态迁移逻辑,由其内部来维护。但调用的时机,交给使用人员
调用的时机,即 constructor()
回调函数的那两个参数
then()
方法根据 promise 的内部状态,执行相应的回调函数
这里的 switch-case-break
相当于 if, if
如果没有 break
,则执行完当前 case
后还会继续执行后面的 case
2. 支持异步 resolve 和 reject
2.1 目标
第二版的目标,是支持异步更新 promise 的状态。比如以下用法:
Copy let p = new MyPromise ((resolve , reject) => {
console .log ( 'init MyPromise' )
console .log ( '1 minute later...' )
// 模拟 ajax, fetch 等,异步更新 promise 的状态
setTimeout (() => {
resolve ( 1 )
} , 1000 )
})
p .then (x => console .log ( `x = ${ x } ` ))
第一版的代码不支持上面的用法,如果用第一版的 MyPromise
执行,会在 1 秒后什么都不输出。因为在执行第 10 行的代码 p.then(...)
时,promise 的状态依然是 pending,所以就不会执行 then()
方法中的任何一个回调函数 onFulfilled
或 onRejected
。
2.2 代码实现
要支持异步修改 promise 的状态,可以在状态是 pending 的时候就把 then()
方法的两个回调函数 onFulfilled
和 onRejected
给存起来,然后等 promise 在真正发生状态迁移的时候——即在执行传给 constructor
函数的两个参数 resolve
, reject
时,再执行它们。
改造后的代码,如下:
Copy function MyPromise (constructor) {
this .state = 'pending'
this .value = undefined
this .reason = undefined
// 1. 新增两个属性,用来存储 then() 的两个回调
// 之所以是数组,是因为同一个 promise 的 then() 方法可能会被调用多次【标准 2.2.6】
this .onFulfilledArray = []
this .onRejectedArray = []
const resolve = (value) => {
if ( this .state === 'pending' ) {
this .state = 'fulfilled'
this .value = value
// 3.1 待“异步”更新状态时,执行对应的回调函数
this . onFulfilledArray .forEach ((cb) => {
cb ( this .value)
})
}
}
const reject = (reason) => {
if ( this .state === 'pending' ) {
this .state = 'rejected'
this .reason = reason
// 3.2 待“异步”更新状态时,执行对应的回调函数
this . onRejectedArray .forEach ((cb) => {
cb ( this .reason)
})
}
}
try {
constructor (resolve , reject)
} catch (e) {
reject (e)
}
}
MyPromise . prototype . then = function (onFulfilled , onRejected) {
onFulfilled = ( typeof onFulfilled === 'function' ) ? onFulfilled : function (x) { return x };
onRejected = ( typeof onRejected === 'function' ) ? onRejected : function (e) { throw e };
switch ( this .state) {
// 2. 新增对 pending 状态的处理,先把回调函数和对应的值给存起来
case 'pending' :
this . onFulfilledArray .push (() => {
setTimeout (() => {
onFulfilled ( this .value)
})
})
this . onRejectedArray .push (() => {
setTimeout (() => {
onRejected ( this .reason)
})
})
break ;
case 'fulfilled' :
setTimeout (() => {
onFulfilled ( this .value)
})
break ;
case 'rejected' :
setTimeout (() => {
onRejected ( this .reason)
})
break ;
}
}
此时,再执行上面的目标代码,就会在 1 秒后输出 x = 1
。
2.3 实现思路
用箭头函数或闭包,把不同状态的“回调函数和值”对应上,并存起来
基于观察者模式,等真正发生状态的改变时,再触发相应的回调函数
需注意:在处理 pending
状态时 setTimeout
的包裹范围
setTimeout
的初衷是为了确保回调参数 onFulfilled()
, onRejected()
的异步执行
而 xxxArray.push()
是需要在当前 then
中执行的,所以在外面
3. 支持链式调用
3.1 目标
第三版的目标,是支持 then()
方法的链式调用,即 p.then().then().then()....then()
。
比如以下用法:
Copy let p = new MyPromise ((resolve , reject) => {
console .log ( 'init MyPromise' )
console .log ( '1 minute later...' )
setTimeout (() => {
resolve ( 1 )
} , 1000 )
})
// 需支持 then() 的链式调用
p .then (x => {
console .log ( `x = ${ x } ` )
return 2
}) .then (y => {
console .log ( `链式调用1, y = ${ y } ` )
}) .then (z => {
console .log ( `链式调用2, z = ${ z } ` )
})
第二版的代码不支持 then()
方法的链式调用,如果执行上面的代码,会在第 14 行报错:
Copy TypeError : Cannot read properties of undefined (reading 'then' )
3.2 代码实现
为了支持 then()
的链式调用,就需要 then()
方法返回一个新的 promise,并把上一个 then()
的返回值传过去。同时,考虑到上个 then
执行的代码是用户“自定义”的回调函数,所以需要用 try-catch 包裹下。
修改 then
方法,代码如下:
Copy MyPromise . prototype . then = function (onFulfilled , onRejected) {
onFulfilled = ( typeof onFulfilled === 'function' ) ? onFulfilled : function (x) { return x };
onRejected = ( typeof onRejected === 'function' ) ? onRejected : function (e) { throw e };
// 1. 新增变量
let promise2
// 2.【标准 2.2.7】`then` must return a promise
switch ( this .state) {
case 'pending' :
// 2.1 promise2 = new MyPromise((resolve, reject) => { ... })
promise2 = new MyPromise ((resolve , reject) => {
this . onFulfilledArray .push (() => {
setTimeout (() => {
// 2.2 在 try-catch 块里 resolve 或 reject
try {
// 2.3 把上个 `then` 的执行结果传下去
let prev = onFulfilled ( this .value)
resolve (prev)
} catch (e) {
reject (e)
}
})
})
this . onRejectedArray .push (() => {
setTimeout (() => {
try {
let prev = onRejected ( this .reason)
resolve (prev)
} catch (e) {
reject (e)
}
})
})
})
break ;
case 'fulfilled' :
promise2 = new MyPromise ((resolve , reject) => {
setTimeout (() => {
try {
let prev = onFulfilled ( this .value)
resolve (prev)
} catch (e) {
reject (e)
}
})
})
break ;
case 'rejected' :
promise2 = new MyPromise ((resolve , reject) => {
setTimeout (() => {
try {
let prev = onRejected ( this .reason)
resolve (prev)
} catch (e) {
reject (e)
}
})
})
break ;
}
// 3. 返回新的 promise
return promise2
}
此时,再执行目标代码,就会在 1 秒后输出:
Copy x = 1
链式调用 1 , y = 2
链式调用 2 , z = undefined
3.3 实现思路
构造一个新的 promise,同时用上一个 then()
的返回值来 resolve
注意事项:
在执行上一个 then
代码的时候,需要 try-catch
在接收上一个 then
的执行结果时,只要不是异常,都会正常 resolve 给下个 then。比如:
Copy Promise .reject ( 1 ) .then (
() => 2 ,
() => 3 ,
) .then ((solution) => {
// Fulfilled with 3
console .log ( `Resolved with ${ solution } ` )
})
4. 完善 then()
回调的返回值
4.1 目标
需要注意的是,在 promise 中,有两个返回值:
then()
方法本身的返回值,需要返回一个新 promise,因为要实现 then()
的链式调用
then()
方法的两个回调参数 onFulfilled
和 onRejected
,它们的返回值可以是原始值、对象或函数,甚至是另一个新 promise
实际上,这两个返回值是有关联的,那就是(本轮)onFulfilled
和 onRejected
的返回值会决定(下一轮)then()
方法的返回值。
考虑下面的用法:
Copy let p = new MyPromise ((resolve , reject) => {
console .log ( 'init MyPromise' )
console .log ( '1 minute later...' )
setTimeout (() => {
resolve ( 1 ) // 原始值 number
} , 1000 )
})
p .then (x => {
console .log ( `第1个then, x = ${ x } ` )
// 返回 function
return () => {
console .log ( '第1个 then 返回 function' )
}
}) .then (y => {
console .log ( `链式调用1, y = ${ y } ` )
// 返回 object
return { 'msg' : '链式调用1 里返回 object' }
}) .then (z => {
console .log ( `链式调用2, z = ${ z } ` )
// 返回 promise(TODO)
return new MyPromise ( function (resolve , reject) {
resolve ( '链式调用2 里返回 promise 的返回值' )
})
}) .then (a => {
console .log ( `链式调用3, a = ${ a } ` )
// 返回原始值 string
return 'well done!'
}) .then (a => {
console .log ( `链式调用4, a = ${ a } ` )
})
如果用第三版的代码,运行以上代码,会在 1 秒后输出如下内容。此时,第 27 行的输出并不符合预期。
Copy 第 1 个then, x = 1
链式调用 1 , y = () => {
console.log ( '第1个 then 返回 function' )
}
链式调用 2 , z = [object Object]
链式调用 3 , a = [object Object]
链式调用 4 , a = well done!
所以,第四版的目标,就是让 onFulfilled
和 onRejected
回调函数的返回值支持 promise。
4.2 代码实现
要完善 onFulfilled
和 onRejected
回调函数的返回值,就需要重新定义下 then()
方法中对 promise2
的 resolve 逻辑。
新增 resolvePromise
函数,代码如下:
Copy /**
* 【标准 2.3】The Promise Resolution Procedure
*
* @param {*} promise 当前 `then` 方法的 promise 值
* @param {*} x 上一个 `then` 方法的执行结果,即当前 `then` 方法的回调函数 `onFulfilled` 或 `onRejected` 的返回值
* @param {*} resolve 当前 `then` 方法要返回的 promise2 的 resolve
* @param {*} reject 当前 `then` 方法要返回的 promise2 的 reject
*/
function resolvePromise (promise , x , resolve , reject) {
//【标准 2.3.1】x 和 promise 不能指向同一个对象
if (x === promise) {
return reject ( new TypeError ( 'Cyclic reference' ))
}
//【标准 2.3.3】x 是一个对象或函数
if (( typeof x === 'object' && x !== null ) || ( typeof x === 'function' )) {
//【标准 2.3.3.3】若多次调用,则只执行第一次的
let thenHasClalled = false
try {
//【标准 2.3.3.1】先存起来,避免多次读取(一次判断类型,一次调用执行)产生副作用
let then = x .then
//【标准 2.3.3.3】x 是 promise, thenable
if ( typeof then === 'function' ) {
then .call (x , (y) => {
if (thenHasClalled) return
thenHasClalled = true
return resolvePromise (promise , y , resolve , reject)
} , (e) => {
if (thenHasClalled) return
thenHasClalled = true
return reject (e)
})
} else {
// x 是普通的对象或函数
return resolve (x)
}
} catch (e) {
if (thenHasClalled) return
thenHasClalled = true
return reject (e)
}
} else {
//【标准 2.3.4】x 是原始值
return resolve (x)
}
}
同时,替换 then
方法里处理 prev
变量的 resolve(prev)
方法,如下:
Copy MyPromise . prototype . then = function (onFulfilled , onRejected) {
...
// resolve(prev)
resolvePromise (promise2 , prev , resolve , reject)
...
}
此时,再执行目标代码,就会在 1 秒后输出:
Copy 第 1 个then, x = 1
链式调用 1 , y = () => {
console.log ( '第1个 then 返回 function' )
}
链式调用 2 , z = [object Object]
链式调用 3 , a = 链式调用 2 里返回 promise 的返回值
链式调用 4 , a = well done!
4.3 相关说明
关于 resolvePromise()
函数的说明:
把 if (x instance of MyPromise)
的逻辑,合并到了 if (typeof then === 'function')
里
return
语句,这里的主要作用是跳出函数结束执行,因为并没有地方用到它的返回值
5. 最终代码
5.1 prototype 初版
Copy // 参考 ECMAScript 规范,改了两个变量名:
// 1. MyPromise() 的参数 constructor -> executor
// 2. state -> status
function MyPromise (executor) {
this .status = 'pending' // 当前状态
this .value = undefined // 状态从 pending -> fulfilled(成功)时的值
this .reason = undefined // 状态从 pending -> rejected(失败)时的值
// 这两个属性,用来存储 then() 的两个回调
// 之所以是数组,是因为同一个 promise 的 then() 方法可能会被调用多次【标准 2.2.6】
this .onFulfilledArray = []
this .onRejectedArray = []
const resolve = (value) => {
// 状态从 pending -> fulfilled,不可逆
if ( this .status === 'pending' ) {
this .status = 'fulfilled'
this .value = value
// 待“异步”更新状态时,执行对应的回调函数
this . onFulfilledArray .forEach ((cb) => {
cb ( this .value)
})
}
}
const reject = (reason) => {
// 状态从 pending -> rejected,不可逆
if ( this .status === 'pending' ) {
this .status = 'rejected'
this .reason = reason
// 待“异步”更新状态时,执行对应的回调函数
this . onRejectedArray .forEach ((cb) => {
cb ( this .reason)
})
}
}
// 捕获构造函数的异常,因为 executor 是用户“自定义”传进来的
try {
executor (resolve , reject)
} catch (e) {
reject (e)
}
}
MyPromise . prototype . then = function (onFulfilled , onRejected) {
//【标准 2.2.1】两个参数都是可选的,如果不是 function 则必须忽略
// 在实践中,会给它们赋个默认值,用来实现 `then` 方法的值穿透
// 也就是说,即便当前的 `then` 方法没传任何参数,promise 也能把值透传给下一个 `then` 方法
onFulfilled = ( typeof onFulfilled === 'function' ) ? onFulfilled : (x) => x;
onRejected = ( typeof onRejected === 'function' ) ? onRejected : (e) => { throw e };
//【标准 2.2.7】`then` must return a promise
let promise2
//【标准 2.2.4】确保 onFulfilled() 和 onRejected() 异步执行,
// 要在 `then` 所在的事件循环之后调用,得用个新堆栈
// 在实践中,可以用宏任务机制(setTimeout, setImmediate)
// 或微任务机制(MutationObserver, process.nextTick)
// 考虑到 promise 本就属于微任务,所以更应该用“微任务”
// 简单起见,这里就用 setTimeout 示意下
switch ( this .status) {
case 'pending' :
promise2 = new MyPromise ((resolve , reject) => {
this . onFulfilledArray .push (() => {
setTimeout (() => {
try {
let prev = onFulfilled ( this .value)
// resolve(prev)
resolvePromise (promise2 , prev , resolve , reject)
} catch (e) {
reject (e)
}
})
})
this . onRejectedArray .push (() => {
setTimeout (() => {
try {
let prev = onRejected ( this .reason)
// resolve(prev)
resolvePromise (promise2 , prev , resolve , reject)
} catch (e) {
reject (e)
}
})
})
})
break ;
case 'fulfilled' :
promise2 = new MyPromise ((resolve , reject) => {
setTimeout (() => {
try {
let prev = onFulfilled ( this .value)
// resolve(prev)
resolvePromise (promise2 , prev , resolve , reject)
} catch (e) {
reject (e)
}
})
})
break ;
case 'rejected' :
promise2 = new MyPromise ((resolve , reject) => {
setTimeout (() => {
try {
let prev = onRejected ( this .reason)
// resolve(prev)
resolvePromise (promise2 , prev , resolve , reject)
} catch (e) {
reject (e)
}
})
})
break ;
}
// 返回新的 promise
return promise2
}
/**
* 【标准 2.3】The Promise Resolution Procedure
*
* @param {*} promise 当前 `then` 方法的 promise 值
* @param {*} x 上一个 `then` 方法的执行结果,即当前 `then` 方法的回调函数 `onFulfilled` 或 `onRejected` 的返回值
* @param {*} resolve 当前 `then` 方法要返回的 promise2 的 resolve
* @param {*} reject 当前 `then` 方法要返回的 promise2 的 reject
*/
function resolvePromise (promise , x , resolve , reject) {
//【标准 2.3.1】x 和 promise 不能指向同一个对象
if (x === promise) {
return reject ( new TypeError ( 'Cyclic reference' ))
}
//【标准 2.3.3】x 是一个对象或函数
if (( typeof x === 'object' && x !== null ) || ( typeof x === 'function' )) {
//【标准 2.3.3.3】若多次调用,则只执行第一次的
let thenHasClalled = false
try {
//【标准 2.3.3.1】先存起来,避免多次读取(一次判断类型,一次调用执行)产生副作用
let then = x .then
//【标准 2.3.3.3】x 是 promise, thenable
if ( typeof then === 'function' ) {
then .call (x , (y) => {
if (thenHasClalled) return
thenHasClalled = true
return resolvePromise (promise , y , resolve , reject)
} , (e) => {
if (thenHasClalled) return
thenHasClalled = true
return reject (e)
})
} else {
// x 是普通的对象或函数
return resolve (x)
}
} catch (e) {
if (thenHasClalled) return
thenHasClalled = true
return reject (e)
}
} else {
//【标准 2.3.4】x 是原始值
return resolve (x)
}
}
// ES6 Module
// export default MyPromise
5.2 prototype 完善微任务
在实现 MyPromise.prototype.then
方法时,为了确保 onFulfilled/onRejected
回调函数的异步执行,用了宏任务 setTimeout
。但这会带来延迟问题,因为两次 Event Loop 之间有时间间隔,其中浏览器约 4ms,Node.js 约 1ms。
所以,还是得用“微任务”机制,宗旨就是:在确保异步执行的同时,“尽早”地调用所有已经加入队列的回调函数。
5.3 prototype 完善封装性
5.4 class 版
6. 测试
使用 promises-aplus-tests 对最终的代码实现进行测试,看它是否符合 Promise/A+ 的规范。
6.1 新增代码
根据文档说明,修改 MyPromise.js
文件,新增以下代码:
Copy // promises-aplus-tests 的测试需要
MyPromise . resolve = function (value) {
return new MyPromise ((resolve , reject) => {
resolve (value)
})
}
MyPromise . reject = function (reason) {
return new MyPromise ((resolve , reject) => {
reject (reason)
})
}
MyPromise . deferred = function () {
let dfd = {}
dfd .promise = new MyPromise ((resolve , reject) => {
dfd .resolve = resolve
dfd .reject = reject
})
return dfd
}
// CommonJS,在 Node.js 环境中用
module . exports = MyPromise
6.2 开始测试
6.2.1 本地安装
Copy npm init --yes
npm install promises-aplus-tests --save-dev # 本地安装
npm run test # 执行脚本,开始测试
package.json
文件的相关配置,如下:
Copy {
"scripts" : {
"test" : "promises-aplus-tests MyPromise.js"
} ,
"devDependencies" : {
"promises-aplus-tests" : "^2.1.2"
}
}
6.2.2 npx
如果不想 npm install
,也可以用 npx 直接运行,命令如下:
Copy npx promises-aplus-tests MyPromise.js # 执行脚本,开始测试
6.3 测试结果
872 个测试用例都成功通过。至此,我们就实现了一个符合 Promises/A+ 规范的 then()
方法。
7. 主要参考