怎样实现 Promise
大多数人都使用过 Promise,觉得扁平化的链式调用解决了 “Callback Hell” 的问题,但是很多人却不理解为啥能实现异步,背后的源码到底是怎么实现的。所以在阅读了网上大量资料之后,尝试着手写一个 JPromise 来加深对 Promise 的理解。
定义构造函数
const PENDING = 0
const RESOLVED = 1
const REJECTED = 2
// simple promise implements
function JPromise (executor) {
this.value = undefined
this.state = PENDING
this.deferred = []
let promise = this
// 为了抓住异常
try {
executor(function resolve(x) {
promise.resolve(x)
}, function reject(reason) {
promise.resolve(reason)
})
} catch (e) {
promise.reject(e)
}
}
Promise 有三种状态值,分别是 pending,resolved,rejected。状态只能从 pending -> resolved 或者 pending -> rejected。构造函数接收一个 executor
函数,这个函数接收两个参数,resolve
和 reject
。这两个参数也是回调函数,暴露给调用方,只要调用方调用了 resolve
或者 reject
,就代表这个 promise settled了。
resolve & reject
JPromise.prototype.resolve = function (x) {
const promise = this
if (this.state === PENDING) {
// 防止多次调用
var magic = false
// 如果 resolve 的参数也是一个 thenable 对象,直接复用他的所有状态
try {
var then = x && x['then']
if (x != null && typeof x === 'object' && typeof then === 'function') {
then.call(x, function (x) {
if (!magic) {
promise.resolve(x)
magic = true
}
}, function (r) {
if (!magic) {
promise.reject(r)
magic = true
}
})
return
}
} catch (error) {
promise.reject(error)
}
this.value = x
this.state = RESOLVED
}
}
JPromise.prototype.reject = function (x) {
if (this.state === PENDING) {
this.value = x
this.state = REJECTED
}
}
从上述可以看,resolve 执行的时候是接收调用方传入的值 x,并且将 promise 状态置为 resolved
。根据 Promise 的规范,resolve 函数是可以接收另外一个 thenable 对象的,所以上面的 try catch 语句就是为了将当前 thenable 的状态传递给 promise。那么什么是 thenable 对象呢。
// thenable 对象,拥有 then 方法,并且方法接收 resolve 和 reject 两个回调函数。
let thenable = {
then: function(resolve, reject) {
resolve(42)
}
}
看上去这个好像实现了 resolve 和 reject 的功能,但是其实还是有缺陷的,缺陷在 then 方法的实现时候会碰到。
then
JPromise.prototype.then = function (onResolved, onRejected) {
const promise = this
return new JPromise(function(resolve, reject) {
promise.deferred.push([onResolved, onRejected, resolve, reject])
// 异步执行
promise.notify()
})
}
JPromise.prototype.notify = function () {
const promise = this
nextTick(() => {
if (promise.state !== PENDING) {
while (promise.deferred.length) {
const [onResolved, onRejected, resolve, reject] = promise.deferred.shift()
try {
if (promise.state === RESOLVED) {
if (typeof onResolved === 'function') {
resolve(onResolved(promise.value))
} else {
resolve(promise.value)
}
} else if(promise.state === REJECTED) {
if (typeof onRejected === 'function') {
// 兼容 JPromise.prototype.catch
resolve(onRejected(promise.value))
} else {
reject(promise.value)
}
}
} catch (error) {
reject(error)
}
}
}
})
}
Promise 规定,then 方法必须返回一个新 promise,而且接收两个回调函数,onResolved 与 onRejected,回调函数的参数分别是之前 promise 的 value 属性,then 方法首先会把新 promise 的 resolve,reject 以及 onResolved 与 onRejected 推入到之前 promise 的 deferred 数组,因为规定 then 方法必须是异步的,所以我用了类似于 Vue 的 next-tick 的实现方案。
const callbacks = []
let pending = false
const flushCallbacks = function () {
pending = false
const copy = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copy.length; i++) {
copy[i]()
}
}
let macroTimerFunc = function () {
setTimeout(flushCallbacks, 0)
}
module.exports = function nextTick (fn) {
callbacks.push(function () {
try {
fn.call()
} catch (error) {
console.error(error)
}
})
if (pending === false) {
pending = true
macroTimerFunc()
}
}
这个 next-tick 函数,主要是将 fn 推入到一个 callbacks 数组,通过 setTimeout 在下一帧 执行。
最终会调用 notify 方法。这个方法很重要,主要就是拿出通过 then 方法存入的 resolve,reject 以及 onResolved 与 onRejected 四个函数,根据当前 promise 的状态去执行不同的逻辑。其实也就是调用 then 方法新生成的 promise 的 resolve 或者 reject 的过程。
乍一看,好像很完美,then 方法也实现了链式调用。但是考虑到 then 方法 是在 setTimeout(fn, 0) 的时间执行的,如果 executor 这个函数存在很长的异步回调呢,比如下面的例子:
let p = new JPromise((resolve) => {
setTimeout(() => {
resolve(1)
}, 3000)
}).then((x) => {
console.log(x)
})
我们分析下上面的执行情况,因为 then 方法是在下一帧就执行,这个时候 promise 还处于 pending 状态,那么 notify 其实啥也没执行。所以我们需要一种机制,就是在 promise 真正被 resolved 或者 rejected 的时候,再调用 nofity,因此我们需要给 resolve 与 reject 方法加行代码。
JPromise.prototype.resolve = function (x) {
const promise = this
if (this.state === PENDING) {
// 防止多次调用
var magic = false
// 如果 resolve 的参数也是一个 thenable 对象,直接复用他的所有状态
try {
var then = x && x['then']
if (x != null && typeof x === 'object' && typeof then === 'function') {
then.call(x, function (x) {
if (!magic) {
promise.resolve(x)
magic = true
}
}, function (r) {
if (!magic) {
promise.reject(r)
magic = true
}
})
return
}
} catch (error) {
promise.reject(error)
}
+ this.notify()
this.value = x
this.state = RESOLVED
}
}
JPromise.prototype.reject = function (x) {
if (this.state === PENDING) {
+ this.notify()
this.value = x
this.state = REJECTED
}
}
catch
catch 方法的实现就很巧妙。
JPromise.prototype.catch = function (onRejected) {
return this.then(null, onRejected)
}
catch 方法也返回了一个新 promise。会走到 notify 函数体下面的这段代码,从而又能实现链式调用。
if (typeof onRejected === 'function') {
// 兼容 JPromise.prototype.catch
resolve(onRejected(promise.value))
}
静态方法—— JPromise.resolve & JPromise.reject
JPromise.resolve = function (x) {
return new JPromise((resolve, reject) => {
resolve(x)
})
}
JPromise.reject = function (x) {
return new JPromise((resolve, reject) => {
reject(x)
})
}
这两个别名很简单,就是生成一个 setted 的 promise。
静态方法—— JPromise.all
JPromise.all = function (iterable) {
if (!Array.isArray(iterable)) return
return new JPromise(function(resolve, reject) {
let count = 0
let result = []
if (iterable.length === 0) {
resolve(result)
}
function resolvers (i) {
return function thenResolve(x) {
result[i] = x
count++
if (iterable.length === count) {
resolve(result)
}
}
}
for (let i = 0; i < iterable.length; i++) {
JPromise.resolve(iterable[i]).then(resolvers(i), reject)
}
})
}
根据 Promise.all 的规范,接收一个 promise 数组,如果有任意一个 promise reject,就触发 reject。所有的 promise 都 resolved,那么在 then 的第一个回调函数的第一个参数就是所有 promise 通过调用 resolve 传出来的值而组成的数组。上面的实现很巧妙,通过一个闭包,每次都会执行 thenResolve 内部的逻辑去检查当前 result 长度是否与传入的 promise 数组的长度相同。在 for 循环当中,只要任意一个 promise rejected,都会走到函数内部返回的 Promise 的 reject 逻辑。
静态方法—— JPromise.race
JPromise.race = function (iterable) {
return new JPromise((resolve, reject) => {
for (let i = 0; i < iterable.length; i++) {
JPromise.resolve(iterable[i]).then(resolve, reject)
}
})
}
race 的实现更巧妙,只要任意的 promise resolved,就执行了新生成的 promise 的 resolve 逻辑了,从而实现了竞态。