前言
手写Promise
,老生常谈的话题,但是实际使用时,难免还是会踩坑,今天就来一步步实现它,摸摸它到底是何方神圣。如果你还没有手写过 Promise,或者你想温习下 Promise 的实现,获取你可以看看这边文章。
接下来就是一步步实现一个符合 PromiseA+ 规范的Promise的过程。
喏,这里:PromiseA+
V0:基本实现
Promise状态:pending
(等待)、fulfilled
(成功)、rejected
(失败)
Promise的雏形
class MyPromise {
constructor(executor) {
this.status = 'pending' // 状态
this.value = undefined // 成功的结果
this.reason = undefined // 失败的原因
let resolve = () => {}
let reject = () => {}
executor(resolve, reject)
}
then(onFulfilled, onRejected) {}
}
executor 容错
// 传入的执行函数可能会抛出错误
try {
// 将resolve和reject给使用者
executor(resolve, reject)
} catch (e) {
// error注入reject
reject(e)
}
resolve 和 reject 方法
// 定义resolve
let resolve = data => {
// 只能从pending变为fulfilled或者rejected状态
if (this.status === 'pending') {
this.status = 'fulfilled'
this.value = data
}
}
// 定义reject
let reject = data => {
// 只能从pending变为fulfilled或者rejected状态
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = data
}
}
then 方法
// 定义then方法,将fulfilled或者rejected的结果传入onFulfilled或者onRejected中
then(onFulfilled, onRejected) {
if (this.status === 'fulfilled') {
onFulfilled(this.value)
}
if (this.status === 'rejected') {
onRejected(this.reason)
}
}
那么,一个简单的实现就完成啦。
小测试:
let p = new MyPromise((resolve, reject) => resolve(1))
p.then(res => console.log(res)) // 1
上面的例子可以正常输出结果,但是,如果Promise内部是异步的呢?
let q = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(2)
}, 0)
})
q.then(res => console.log(res)) // 并没有输出
由于异步延迟,调用then方法时,状态还是 pending,无法调用onFulfilled或者onRejected,那么我们需要对异步的情况做相应的处理。
V1:加入异步处理
怎么处理异步情况?
针对上面的分析,调用then方法时还是 pending 状态,那么此时应该将回调函数存起来,等到状态改变(fulfilled / rejected)时再取出来调用。考虑到可能存在多个回调函数,我们使用数组存储回调函数,形成回调队列。
1. 定义两个数组作为回调队列
this.onResolvedCallbacks = [] // 存放成功的回调
this.onRejectedCallbacks = [] // 存放失败的回调
2. then 方法中处理 pending 状态下的回调函数
then(onFulfilled, onRejected) {
if (this.status === 'fulfilled') {
onFulfilled(this.value)
}
if (this.status === 'rejected') {
onRejected(this.reason)
}
// 新增:当 Promise还是等待状态,存储回调函数
if (this.status === 'pending') {
this.onResolvedCallbacks.push(onFulfilled)
this.onRejectedCallbacks.push(onRejected)
}
}
3. 调用回调队列的函数
什么时候调用回调队列?
由于 resolve 或者 reject 时是在异步队列里,我们在 then 中已经存储了相应的回调函数,那么当状态改变时,即在 resolve 或者 reject 发生时,就可以将回调函数取出来依次调用。
修改下constructor 方法:
let resolve = data => {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.value = data
// 状态改变后取出回调队列的函数依次调用
this.onResolvedCallbacks.forEach(cb => cb(this.value))
}
}
let reject = data => {
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = data
// 状态改变后取出回调队列的函数依次调用
this.onRejectedCallbacks.forEach(cb => cb(this.reason))
}
}
我们在存储回调函数时,value 或者 reason还没有值,等到状态改变时才拿到值,因此依次调用回调队列的函数时将相应的 value 或者 reason 传入。
测试一下加入异步是否可以正常输出
let q = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(2)
}, 0)
})
q.then(res => console.log(res)) // 2
异步处理,完成。
就这?但是,别忘了还有 Promise 的链式调用呢!
V2:实现链式调用
基于前面的实现是没法实现链式调用的。
q.then(res => {
console.log(res)
return 3
}).then(res => console.log(res)) // Uncaught TypeError: Cannot read property 'then' of undefined
别忘了Promises/A+规范要求:then 方法返回新的 Promise。
接下来完善 then 方法
规范化then 参数:onFulfilled 和 onRejected
// onFulfilled 和 onRejected 为函数
// onFulfilled 不是函数时包装成函数,返回传入的值
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : value => value
// onRejected 不是函数时,需要抛出错误,否则会在后面的链式调用被resolve捕获
onRejected =
typeof onRejected === 'function' ? onRejected : error => {throw error}
then 方法返回新的 Promise,并加入 try...catch
then(onFulfilled, onRejected) {
// onFulfilled 和 onRejected 为函数
// onFulfilled 不是函数时包装成函数,返回传入的值
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : value => value
// onRejected 不是函数时需要抛出错误,否则会在后面的链式调用被resolve捕获!!!
onRejected =
typeof onRejected === 'function'
? onRejected
: error => {
throw error
}
const promise = new MyPromise((resolve, reject) => {
if (this.status === 'fulfilled') {
try {
let x = onFulfilled(this.value)
resolve(x)
} catch (e) {
reject(e)
}
}
if (this.status === 'rejected') {
try {
let x = onRejected(this.reason)
resolve(x)
} catch (e) {
reject(e)
}
}
// 当 Promise还是等待状态,存储回调函数
if (this.status === 'pending') {
this.onResolvedCallbacks.push(() => {
try {
let x = onFulfilled(this.value)
resolve(x)
} catch (e) {
reject(e)
}
})
this.onRejectedCallbacks.push(() => {
try {
let x = onRejected(this.reason)
resolve(x)
} catch (e) {
reject(e)
}
})
}
})
return promise
}
这下再执行链式调用就不报错了
q.then(res => {
console.log(res)
return 3
}).then(res => console.log(res)) // 一次输出 2 3
q.then()then().then(res => console.log(res)) // 2, 依次传递,因此输出2
到这里,基本就实现了支持链式调用的Promise了。
但是,但是,但是,是的,又双叒但是了,onFulfilled 和 onRejected 里面我们可以返回任何值,原始数据类型、引用类型、甚至是Promise!基于上面的实现,还不足以处理所有的返回值。不信你看:
q.then(res => {
console.log(res)
return new MyPromise((resolve) => resolve(3))
}).then(res => console.log(res)) // 依次输出 2 MyPromise
但是实际上,Promise 针对 onFulfilled 和 onRejected 里面返回 Promise 时,是会步步往里走,直到拿到一个里面的值的。也就是说,他实际上会输出 2 3。
这就迎来了下一版本。
我们将 then 中的 resolve 的逻辑抽取出来,用完善版的 resolvePromise 代替。
V3:引入 resolvePromise 方法
我们希望这个函数处理什么呢?
- 处理 onFulfilled 和 onRejected 的返回值 x,onFulfilled 时需要 resolve,onRejected 时需要 reject
- 循环引用:当 then 的返回值 promise 与 x 是同一引用时,抛出TypeError错误(2.3.1)
resolvePromise需要的参数
基于上面的分析,这个函数长这样
/**
* 处理then里面onFulfilled或onRejected的返回值
* @param {Object} promise then方法返回的Promise对象
* @param {*} x onFulfilled或onRejected的返回值
* @param {Function} resolve Promise构造函数的resolve方法
* @param {Function} reject Promise构造函数的reject方法
*/
function resolvePromise(promise, x, resolve, reject) {
//
}
循环引用时抛出 TypeError 错误
if (promise === x) {
return reject(new TypeError('Promise循环引用啦'))
}
处理返回值
大概如此:
// 返回值x 为 Promise(2.3.2)
// 这一节其实可以省略,因为在下面对then的处理中已经包含了
// if (x instanceof Promise) {}
// 返回值x是对象或者函数(2.3.3)包含x为Promise的情况(2.3.2)
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// try...catch 防止then出现异常
try {
let then = x.then // (2.3.3.1)
/
} catch (e) {
reject(e) // (2.3.3.2)(2.3.3.3.4.2)
}
} else {
// 返回值x只是一个普通值(2.3.4)
resolve(x)
}
接下来,处理下返回值的复杂场景
// 返回值x 为 Promise(2.3.2)
// 这一节其实可以省略,因为在下面对then的处理中已经包含了
// if (x instanceof Promise) {}
let called = false // 是否 resolve 或者 reject了
// 返回值x是对象或者函数(2.3.3)包含x为Promise的情况(2.3.2)
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// try...catch 防止then出现异常
try {
let then = x.then // (2.3.3.1)
if (typeof then === 'function') {
// 返回值x有then且then是一个函数,则调用它,并将this指向x(2.3.3.3)
then.call(
x,
y => {
if (called) return // 已经 resolve 或者 reject了, 则忽略(2.3.3.3.3)
called = true
// y有可能还是一个Promise,递归处理
resolvePromise(promise, y, resolve, reject)
},
r => {
if (called) return // 已经 resolve 或者 reject了, 则忽略(2.3.3.3.3)
called = true
reject(r)
}
)
} else {
// 只是一个普通对象或者普通函数,则直接resolve
resolve(x)
}
} catch (e) {
if (called) return // 已经 resolve 或者 reject了, 则忽略(2.3.3.3.4.1)
called = true
reject(e) // (2.3.3.2)(2.3.3.3.4.2)
}
} else {
// 返回值x只是一个普通值(2.3.4)
resolve(x)
}
V4: 给then中所有的返回加上异步延迟(标准版)
这里使用setTimeout模拟延迟
then(onFulfilled, onRejected) {
// ...
let promise
promise = new MyPromise((resolve, reject) => {
if (this.status === 'fulfilled') {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(promise, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}
if (this.status === 'rejected') {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}
// 当 Promise还是等待状态,存储回调函数
if (this.status === 'pending') {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(promise, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
}
})
return promise
}
标准版实现:MyPromise
测试是否符合PromiseA+规范
使用 promises-aplus-tests
这个库,测试你的Promise是否符合PromiseA+
yarn add promises-aplus-tests
测试前,需要在你的Promise结尾加上:
MyPromise.defer = MyPromise.deferred = function () {
let defer = {}
defer.promise = new MyPromise((resolve, reject) => {
defer.resolve = resolve
defer.reject = reject
})
return defer
}
try {
module.exports = MyPromise
} catch (e) {}
下面就可以测试了
npx promises-aplus-tests Promise.js
基于上面的实现,可以看到一连串绿勾勾,最后看到 872 passing (17s)
,通过了测试。
V5: 加上周边方法(现代完整版)
其实基于V4已经够用了,也通过了测试。但是使用Promise时,我们是可以直接使用Promise.resolve
、Promise、reject
等方法的,V5即在V4的基础上,补充了resolve、reject、catch、finally、all、race、allSettled
等的实现。这里称之为 “现代完整版”。
现代完整版实现:Promise_pro
Q & A
1. 为何又谈论这个被谈烂的话题?
时常回顾和温习,是非天资聪颖者最后的倔强。若同样有助于你,万分荣幸。
2. 为什么给then中所有的返回加上延迟要使用setTimeout?
首先,Promise 本身是同步的,其 then 和 catch 方法是异步的, 这里使用 setTimeout 模拟异步,是符合Promise A+规范的,也通过了测试,当然你也可以使用其它方法,如 MutationObserver。
虽然在 Eventloop 中setTimeout 属于宏任务,而实际上,Promise 的 then和 catch 是属于微任务队列,该实现与实际是有些许差异的,但这并影响我们通过手写一个符合 Promise A+ 规范的 Promise 去理解它的原理。
参考
感谢大佬铺路,助我前行