Promise 是一种非常重要的异步编程模式,可以巧妙地避免产生的回调地狱问题,有如下几个特点:
- Promise 是基于回调的
- 利用发布订阅模式来进行通知回调
- 递归链式调用
Promise/A+ 规范中对 Promise 做了如下的定义:
一个有 then 方法的对象或者函数,只要是符合 A+ 规范,就是 Promise
A+ 规范里面有很规则,主要讲述了 Promise 的内部变量和 then 函数的执行流程,并没有定义 catch/finally 等方法,其实这些方法都是基于 Promise 和 then 函数的,具体用法可参考 MDN 文档。
接下来就来实现一个 Promise,首先定义构造函数,里面保存四个私有变量:
- Promise 的状态(
status),该状态只能被改一次 - 成功的值
value - 拒绝的原因
reason - 异步时预先保存的成功或失败的回调
cb
function Promise(executor) {
this.status = 'pending' // 初始状态为即将发生
this.value = undefined // 用于保存已完成的值
this.reason = undefined // 用于保存未完成的原因
this.cb = { onFulfilled: [], onRejected: [] } // 用于保存完成或失败之后的回调函数
const resolve = (value) => {
if (value instanceof Promise) return value.then(resolve, reject)
if (this.status !== 'pending') return // 状态一:从 pending 到 fulfilled
this.status = 'fulfilled'
this.value = value
this.cb.onFulfilled.forEach((fn) => fn())
}
const reject = (reason) => {
if (this.status !== 'pending') return // 状态二:只能从 pending 到 rejected
this.status = 'rejected'
this.reason = reason
this.cb.onRejected.forEach((fn) => fn())
}
try {
executor(resolve, reject) // 用于创建 Promise 时的执行器,把 resolve 和 reject 闭包传给用户
} catch (e) {
reject(e)
}
}
可以看到,Promise 函数接收一个 executor 执行器,创建 Promise 的时候会同步立即执行,并把内部定义的 resolve 和 reject 闭包传到 executor 里面,从而让用户改变 Promise 内部的状态。
Promise 的核心是链式调用,接下来在其原型上添加 then 方法:
Promise.prototype.then = function (onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : (v) => v // 设置默认已完成回调
onRejected = typeof onRejected === 'function' ? onRejected : (err) => {throw err} // 设置默认未完成回调
const params = { promise: this, onFulfilled, onRejected } // 组装参数,用于延迟执行
const promise2 = new Promise((resolve, reject) => { // 创建新的 Promise 用于 then 链式调用
Object.assign(params, { resolve, reject })
if (this.status === 'pending') {
this.cb.onFulfilled.push(() => delay(params)) // 封装已完成回调,将来延迟执行
this.cb.onRejected.push(() => delay(params)) // 封装未完成回调,将来延迟执行
} else {
delay(params) // 调用延迟执行函数
}
})
return params.promise2 = promise2 // 把 promise2 挂到 params 参数里并返回
}
可以看到,Promise 能够链式调用的本质就是 then 方法里面返回了一个新的 promise2,且必须等待当前 promise 成功或失败才可以,也就是说只有 then 的 onFulfilled 或 onRejected 回调函数之一被执行并取到返回值之后,才能调用 promise2 的 resolve 或者 reject 方法。
按照 Promise/A+ 规范,then 的 onFulfilled 或 onRejected 回调必须是异步执行的,所以接下来选择异步策略 schedule,即用 process.nextTick 还是 setTimeout 还是 MutationObserver,这一点跟 Vue 当中选择 $nextTick 的异步策略类似。一旦确定异步策略之后,就要定义延迟执行函数 delay,把当前 promise 以及它的 onFulfilled 和 onRejected 回调、新的 promise2 以及它的 resolve 和 reject 闭包取到,按照规范描述进行处理:
let schedule = (fn) => process.nextTick(fn) // 可以换成 setTimeout: let schedule = (fn) => setTimeout(fn, 0)
function delay(params) {
schedule(() => {
const { promise, promise2, onFulfilled, onRejected, resolve, reject } = params
try {
let x = promise.status === 'fulfilled' ? onFulfilled(promise.value) : onRejected(promise.reason)
resolvePromise(x, promise2, resolve, reject) // 返回值为 promise 时递归处理
} catch (e) {
reject(e)
}
})
}
这里选择的默认延迟执行函数是 process.nextTick,它是一个微任务,更接近原生 Promise 的使用场景,不过只能用在 Node.js 环境下,浏览器中可以用 MutationObserver 或 setTimeout,其实更好的处理方式是暴露一个 API 让用户自己选择微任务或宏任务来实现,例如:
Promise.setScheduler = (_schedule) => (schedule = _schedule)
接下来就是实现递归解析 promise 的处理逻辑了:
function resolvePromise(x, promise2, resolve, reject) {
if (x === promise2) return reject(new TypeError(`TypeError: Chaining cycle detected`)) // 规范约定不能相等
if (!(x && ['object', 'funciton'].includes(typeof x))) resolve(x) // 规范约定非对象和函数直接 resolve
let called // 确保 resolve 或 reject 只能被调用一次
try {
let then = x.then
if (typeof then !== 'function') return resolve(x) // 规范约定 then 非函数时直接 resolve
then.call(x,
(y) => {
if (called) return
called = true
resolvePromise(y, promise2, resolve, reject) // 递归解析 promise 的值
},
(r) => {
if (called) return
called = true && reject(r)
}
)
} catch (e) {
if (called) return
called = true && reject(e)
}
}
以上就是 Promise 的全部实现了,不多不少,刚好 75 行代码,可以用下面的方法进行测试:
-
全局安装 Promise/A+ 测试包
yarn global add promises-aplus-tests -
按照要求实现 Adaptor 适配器并导出 Promise 函数
Promise.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-tests promise.js