-- What i can't create, i don't understant
前言
实现Promise的目的是为了深入的理解Promies,以在项目中游刃有余的使用它。完整的代码见gitHub
Promise标准
Promise的标准有很多个版本,本文采用ES6原生Promise使用的Promise/A+标准。完整的Promise/A+标准见这里,总结如下:
- promise具有状态state(status),状态分为pending, fulfilled(我比较喜欢叫做resolved), rejected。初始为pending,一旦状态改变,不能再更改为其它状态。当promise为fulfilled时,具有value;当promise为rejected时,具有reason;value和reason都是一旦确定,不能改变的。
- promise具有then方法,注意了,只有then方法是必须的,其余常用的catch,race,all,resolve等等方法都不是必须的,其实这些方法都可以用then方便的实现。
- 不同的promise的实现需要可以相互调用
OK,搞清楚了promise标准之后,开始动手吧
Promise构造函数
产生一个对象有很多种方法,构造函数是看起来最面向对象的一种,而且原生Promise实现也是使用的构造函数,因此我也决定使用构造函数的方法。
首先,先写一个大概的框架出来:
// 总所周知,Promise传入一个executor,有两个参数resolve, reject,用来改变promise的状态
function Promise(executor) {
this.status = 'pending'
this.value = void 0 // 为了方便把value和reason合并
const resolve = function() {}
const reject = function() {}
executor(resolve, reject)
}
很明显,这个构造函数还有很多问题们一个一个来看
-
resolve和reject并没有什么卵用。
首先,用过promise的都知道,resolve和reject是用来改变promise的状态的:function Promise(executor) { this.status = 'pending' this.value = void 0 // 为了方便把value和reason合并 const resolve = value => { this.value = value this.status = 'resolved' } const reject = reason => { this.value = reason this.status = 'rejected' } executor(resolve, reject) }
然后,当resolve或者reject调用的时候,需要执行在then方法里传入的相应的函数(通知)。有没有觉得这个有点类似于事件(发布-订阅模式)呢?
function Promise(executor) { this.status = 'pending' this.value = void 0 // 为了方便把value和reason合并 this.resolveListeners = [] this.rejectListeners = [] // 通知状态改变 const notify(target, val) => { target === 'resolved' ? this.resolveListeners.forEach(cb => cb(val)) : this.rejectListeners.forEach(cb => cb(val)) } const resolve = value => { this.value = value this.status = 'resolved' notify('resolved', value) } const reject = reason => { this.value = reason this.status = 'rejected' notify('rejected', reason) } executor(resolve, reject) }
-
status和value并没有做到一旦确定,无法更改。这里有两个问题,一是返回的对象暴露了status和value属性,并且可以随意赋值;二是如果在executor里多次调用resolve或者reject,会使value更改多次。
第一个问题,如何实现只读属性:function Promise(executor) { if (typeof executor !== 'function') { throw new Error('Promise executor must be fucntion') } let status = 'pending' // 闭包形成私有属性 let value = void 0 ...... // 使用status代替this.value const resolve = val => { value = val status = 'resolved' notify('resolved', val) } const reject = reason => { value = reason status = 'rejected' notify('rejected', reason) } // 通过getter和setter设置只读属性 Object.defineProperty(this, 'status', { get() { return status }, set() { console.warn('status is read-only') } }) Object.defineProperty(this, 'value', { get() { return value }, set() { console.warn('value is read-only') } })
第二个问题,避免多次调用resolve、reject时改变value,而且标准里(2.2.2.3 it must not be called more than once)也有规定,then注册的回调只能执行一次。
const resolve = val => { if (status !== 'pending') return // 避免多次运行 value = val status = 'resolved' notify('resolved', val) }
-
then注册的回调需要异步执行。
说到异步执行,对原生Promise有了解的同学都知道,then注册的回调在Micro-task中,并且调度策略是,Macro-task中执行一个任务,清空所有Micro-task的任务。简而言之,promise异步的优先级更高。
其实,标准只规定了promise回调需要异步执行,在一个“干净的”执行栈执行,并没有规定一定说要用micro-task,并且在低版本浏览器中,并没有micro-task队列。不过在各种promise的讨论中,由于原生Promise的实现,micro-task已经成成为了事实标准,而且promise回调在micro-task中也使得程序的行为更好预测。
在浏览器端,可以用MutationObserver实现Micro-task。本文利用setTimeout来简单实现异步。
const resolve = val => { if (val instanceof Promise) { return val.then(resolve, reject) } // 异步执行 setTimeout(() => { if (status !== 'pending') return status = 'resolved' value = val notify('resolved', val) }, 0) }
最后,加上错误处理,就得到了一个完整的Promise构造函数:
function Promise(executor) {
if (typeof executor !== 'function') {
throw new Error('Promise executor must be fucntion')
}
let status = 'pending'
let value = void 0
const notify = (target, val) => {
target === 'resolved'
? this.resolveListeners.forEach(cb => cb(val))
: this.rejectListeners.forEach(cb => cb(val))
}
const resolve = val => {
if (val instanceof Promise) {
return val.then(resolve, reject)
}
setTimeout(() => {
if (status !== 'pending') return
status = 'resolved'
value = val
notify('resolved', val)
}, 0)
}
const reject = reason => {
setTimeout(() => {
if (status !== 'pending') return
status = 'rejected'
value = reason
notify('rejected', reason)
}, 0)
}
this.resolveListeners = []
this.rejectListeners = []
Object.defineProperty(this, 'status', {
get() {
return status
},
set() {
console.warn('status is read-only')
}
})
Object.defineProperty(this, 'value', {
get() {
return value
},
set() {
console.warn('value is read-only')
}
})
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
总的来说,Promise构造函数其实只干了一件事:执行传入的executor,并构造了executor的两个参数。
实现then方法
首先需要确定的是,then方法是写在构造函数里还是写在原型里。
写在构造函数了里有一个比较大的好处:可以像处理status和value一样,通过闭包让resolveListeners和rejectListeners成为私有属性,避免通过this.rejectListeners来改变它。
写在构造函数里的缺点是,每一个promise对象都会有一个不同的then方法,这既浪费内存,又不合理。我的选择是写在原型里,为了保持和原生Promise有一样的结构和接口。
ok,还是先写一个大概的框架:
Promise.prototype.then = function (resCb, rejCb) {
this.resolveListeners.push(resCb)
this.rejectListeners.push(rejCb)
return new Promise()
}
随后,一步一步的完善它:
-
then方法返回的promise需要根据resCb或rejCb的运行结果来确定状态。
Promise.prototype.then = function (resCb, rejCb) { return new Promise((res, rej) => { this.resolveListeners.push((val) => { try { const x = resCb(val) res(x) // 以resCb的返回值为value来resolve } catch (e) { rej(e) // 如果出错,返回的promise以异常为reason来reject } }) this.rejectListeners.push((val) => { try { const x = rejCb(val) res(x) // 注意这里也是res而不是rej哦 } catch (e) { rej(e) // 如果出错,返回的promise以异常为reason来reject } }) }) }
ps:众所周知,promise可以链式调用,说起链式调用,我的第一个想法就是返回this就可以了,但是then方法不可以简单的返回this,而要返回一个新的promise对象。因为promise的状态一旦确定就不能更改,而then方法返回的promise的状态需要根据then回调的运行结果来决定。
-
如果resCb/rejCb返回一个promiseA,then返回的promise需要跟随(adopt)promiseA,也就是说,需要保持和promiseA一样的status和value。
this.resolveListeners.push((val) => { try { const x = resCb(val) if (x instanceof Promise) { x.then(res, rej) // adopt promise x } else { res(x) } } catch (e) { rej(e) } }) this.rejectListeners.push((val) => { try { const x = resCb(val) if (x instanceof Promise) { x.then(res, rej) // adopt promise x } else { res(x) } } catch (e) { rej(e) } })
-
如果then的参数不是函数,需要忽略它,类似于这种情况:
new Promise(rs => rs(5)) .then() .then(console.log)
其实就是把value和状态往后传递
this.resolveListeners.push((val) => { if (typeof resCb !== 'function') { res(val) return } try { const x = resCb(val) if (x instanceof Promise) { x.then(res, rej) // adopt promise x } else { res(x) } } catch (e) { rej(e) } }) // rejectListeners也是相同的逻辑
-
如果调用then时, promise的状态已经确定,相应的回调直接运行
// 注意这里需要异步 if (status === 'resolved') setTimeout(() => resolveCb(value), 0) if (status === 'rejected') setTimeout(() => rejectCb(value), 0)
最后,就得到了一个完整的then方法,总结一下,then方法干了两件事,一是注册了回调,二是返回一个新的promise对象。
// resolveCb和rejectCb是相同的逻辑,封装成一个函数
const thenCallBack = (cb, res, rej, target, val) => {
if (typeof cb !== 'function') {
target === 'resolve'
? res(val)
: rej(val)
return
}
try {
const x = cb(val)
if (x instanceof Promise) {
x.then(res, rej) // adopt promise x
} else {
res(x)
}
} catch (e) {
rej(e)
}
}
Promise.prototype.then = function (resCb, rejCb) {
const status = this.status
const value = this.value
let thenPromise
thenPromise = new Promise((res, rej) => {
/**
* 这里不能使用bind来实现柯里画,规范里规定了:
* 2.2.5: onFulfilled and onRejected must be called as functions (i.e. with no this value))
*/
const resolveCb = val => {
thenCallBack(resCb, res, rej, 'resolve', val)
}
const rejectCb = val => {
thenCallBack(rejCb, res, rej, 'reject', val)
}
if (status === 'pending') {
this.resolveListeners.push(resolveCb)
this.rejectListeners.push(rejectCb)
}
if (status === 'resolved') setTimeout(() => resolveCb(value), 0)
if (status === 'rejected') setTimeout(() => rejectCb(value), 0)
})
return thenPromise
}
不同的Promise实现可以互相调用
首先要明白的是什么叫互相调用,什么情况下会互相调用。之前实现then方法的时候,有一条规则是:如果then方法的回调返回一个promiseA。then返回的promise需要adopt这个promiseA,也就是说,需要处理这种情况:
new MyPromise(rs => rs(5))
.then(val => {
return Promise.resolve(5) // 原生Promise
})
.then(val => {
return new Bluebird(r => r(5)) // Bluebird的promise
})
关于这个,规范里定义了一个叫做The Promise Resolution Procedure的过程,我们需要做的就是把规范翻译一遍,并替代代码中判断promise的地方
const resolveThenable = (promise, x, resolve, reject) => {
if (x === promise) {
return reject(new TypeError('chain call found'))
}
if (x instanceof Promise) {
return x.then(v => {
resolveThenable(promise, v, resolve, reject)
}, reject)
}
if (x === null || (typeof x !== 'object' && typeof x !== 'function')) {
return resolve(x)
}
let called = false
try {
// 这里有一个有意思的技巧。标准里解释了,如果then是一个getter,那么通过赋值可以保证getter只被触发一次,避免副作用
const then = x.then
if (typeof then !== 'function') {
return resolve(x)
}
then.call(x, v => {
if (called) return
called = true
resolveThenable(promise, v, resolve, reject)
}, r => {
if (called) return
called = true
reject(r)
})
} catch (e) {
if (called) return
reject(e)
}
}
到这里,一个符合标准的Promise就完成了,完整的代码如下:
function Promise(executor) {
if (typeof executor !== 'function') {
throw new Error('Promise executor must be fucntion')
}
let status = 'pending'
let value = void 0
const notify = (target, val) => {
target === 'resolved'
? this.resolveListeners.forEach(cb => cb(val))
: this.rejectListeners.forEach(cb => cb(val))
}
const resolve = val => {
if (val instanceof Promise) {
return val.then(resolve, reject)
}
setTimeout(() => {
if (status !== 'pending') return
status = 'resolved'
value = val
notify('resolved', val)
}, 0)
}
const reject = reason => {
setTimeout(() => {
if (status !== 'pending') return
status = 'rejected'
value = reason
notify('rejected', reason)
}, 0)
}
this.resolveListeners = []
this.rejectListeners = []
Object.defineProperty(this, 'status', {
get() {
return status
},
set() {
console.warn('status is read-only')
}
})
Object.defineProperty(this, 'value', {
get() {
return value
},
set() {
console.warn('value is read-only')
}
})
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
const thenCallBack = (cb, res, rej, target, promise, val) => {
if (typeof cb !== 'function') {
target === 'resolve'
? res(val)
: rej(val)
return
}
try {
const x = cb(val)
resolveThenable(promise, x, res, rej)
} catch (e) {
rej(e)
}
}
const resolveThenable = (promise, x, resolve, reject) => {
if (x === promise) {
return reject(new TypeError('chain call found'))
}
if (x instanceof Promise) {
return x.then(v => {
resolveThenable(promise, v, resolve, reject)
}, reject)
}
if (x === null || (typeof x !== 'object' && typeof x !== 'function')) {
return resolve(x)
}
let called = false
try {
// 这里有一个有意思的技巧。标准里解释了,如果then是一个getter,那么通过赋值可以保证getter只被触发一次,避免副作用
const then = x.then
if (typeof then !== 'function') {
return resolve(x)
}
then.call(x, v => {
if (called) return
called = true
resolveThenable(promise, v, resolve, reject)
}, r => {
if (called) return
called = true
reject(r)
})
} catch (e) {
if (called) return
reject(e)
}
}
Promise.prototype.then = function (resCb, rejCb) {
const status = this.status
const value = this.value
let thenPromise
thenPromise = new Promise((res, rej) => {
const resolveCb = val => {
thenCallBack(resCb, res, rej, 'resolve', thenPromise, val)
}
const rejectCb = val => {
thenCallBack(rejCb, res, rej, 'reject', thenPromise, val)
}
if (status === 'pending') {
this.resolveListeners.push(resolveCb)
this.rejectListeners.push(rejectCb)
}
if (status === 'resolved') setTimeout(() => resolveCb(value), 0)
if (status === 'rejected') setTimeout(() => rejectCb(value), 0)
})
return thenPromise
}
关于promise的一些零散知识
- Promise.resolve就是本文所实现的resolveThenable,并不是简单的用来返回一个resolved状态的函数,它返回的promise对象的状态也并不一定是resolved。
- promise.then(rs, rj)和promise.then(rs).catch(rj)是有区别的,区别在于当rs出错时,后一种方法可以进行错误处理。
感想与总结
实现Promise的过程其实并没有我预想的那么难,所谓的Promise的原理我感觉就是类似于观察者模式,so,不要有畏难情绪,我上我也行^_^。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。