本文旨在通过实现一个 Promise 类来深入理解 Promise 的运行机制,虽然会提到一些 Promise 的基本概念,但最好先有一定了解, 可通过 MDN 进行查询
术语
由于对 Reolve、Fulfill、Reject 之类的术语并没有十分统一的中文译名,本文将保留其英文名
Promise 的本质是一个状态机
Promise 总共具有三个状态:Pending、Fulfilled、Rejected。在创建 Promise 对象时,会传入一个由开发者定义的函数 fn。开发者在其中自由的编写业务逻辑,可以是文件读取、网络请求,并通过调用 fn 的两个参数来控制 Promise 改变状态的实际。 Promise 本质就是一个这样的辅助工具,让开发者专注于业务逻辑。
我们可以在 Chrome Devtool 中 print promise 对象来查看它当前的状态和终值(Eventual Value)
Promise.resolve(1)
> Promise {<fulfilled>: 1}
所以实现 Promise 第一步就是实现一个状态机,定义这些状态,与转换状态的方法
class MyPromise {
#state
#val // promise
constructor(fn) {
this.#state = 'pending'
this.val = null
fn(this.#fulfill, this.#reject)
}
#fulfill = (value) => {
this.#state = 'fulfilled'
this.val = value
}
#reject = (value) => {
this.#state = 'rejected'
this.val = value
}
}
.then 函数
Promise 允许开发者在任意时间添加对 Promise 状态改变的监听,通过调用.then(onFullfilled, OnRejected)并传入两个回调函数实现。
注意开发者可多次调用.then()函数,如:
const p = Promise.resolve('resolved')
p.then((v) => console.log(v))
p.then((v) => console.log('Yeah!'))
> resolved
> Yeah!
因此我们需要就这些 Handler 一次存储起来,在状态改变时依次调用。
// mark
function addToTaskQueue(fn) {
// mark
// link[11:14] https://zh.javascript.info/microtask-queue
// 模拟浏览器微任务
// mark
setTimeout(fn, 0)
// mark
}
class MyPromise {
#state
#value // promise
// mark
#handlers = []
constructor(fn) {
this.#state = 'pending'
this.#value = null
fn(this.#fulfill, this.#reject)
}
#fulfill = (value) => {
this.#state = 'fulfilled'
this.#value = value
// mark
this.#handlers.forEach((handler) => {
// mark
handler.onFulfilled(this.#val)
// mark
})
// mark
this.#handlers = null // garbage collecting
}
#reject = (value) => {
this.#state = 'rejected'
this.#value = value
// mark
this.#handlers.forEach((handler) => {
// mark
handler.onRejected(this.#val)
// mark
})
// mark
this.#handlers = null // garbage collecting
}
// mark
then = (onFulfilled, onRejected) => {
// mark
addToTaskQueue(() => {
// mark
if (this.#state === 'pending') {
// mark
this.#handlers.push({ onFulfilled, onRejected })
// mark
}
// mark
if (this.#state === 'fulfilled') {
// mark
onFulfilled(this.#value)
// mark
}
// mark
if (this.#state === 'rejected') {
// mark
onRejected(this.#value)
// mark
}
// mark
})
// mark
}
}
// Test Code
const p = new MyPromise((resolve, reject) => {
setTimeout(() => resolve('resolved!'), 1000)
})
p.then(
(res) => {
console.log(res)
},
(err) => {
console.log(err)
}
)
console.log('global')
由于 Promise 状态的改变可能在调用then之前或之后,所以需要在多处进行判断。
在进行下一步之前,对代码的通用部分进行复用,添加类型检查:
class MyPromise {
#state
#value
#handlers
constructor(fn) {
this.#state = 'pending'
this.#value = null
this.#handlers = []
fn(this.#fulfill, this.#reject)
}
#fulfill = (value) => {
this.#state = 'fulfilled'
this.#value = value
// mark
this.#handlers.forEach(this.#handle)
this.#handlers = null // garbage collecting
}
#reject = (err) => {
this.#state = 'rejected'
this.#value = err
// mark
this.#handlers.forEach(this.#handle)
this.#handlers = null // garbage collecting
}
// mark
#handle = (handler) => {
if (this.#state === 'pending') {
this.#handlers.push(handler)
}
if (this.#state === 'fulfilled') {
handler.onFulfilled(this.#value)
}
if (this.#state === 'rejected') {
handler.onRejected(this.#value)
}
}
then = (onFulfilled, onRejected) => {
addToTaskQueue(() => {
// mark
this.#handle({
onFulfilled,
onRejected,
})
})
}
}
.then的链式调用
Promise 的一个重要特性,就是允许链式的调用,从而一定程度上避免了回调地狱的发生。为了做到这一点,我们需要将回调函数 onFulfilled/OnRejected 的返回值穿透到最外层,即创建并返回一个以回调函数返回值为内部状态的 Promise,以便进一步调用后续的.then方法。
then = (onFulfilled, onRejected) => {
// mark
const nextPromise = new MyPromise((resolve, reject) => {
// mark
const fullfillmentTask = () => {
// mark
const value =
// mark
typeof onFulfilled === 'function' ? onFulfilled(this.#value) : this.#value
// mark
resolve(value)
// mark
}
// mark
const rejectionTask = () => {
// mark
const value =
// mark
typeof onRejected === 'function' ? onRejected(this.#value) : this.#value
// mark
resolve(value)
}
addToTaskQueue(() => {
this.#handle({
onFulfilled: fullfillmentTask,
onRejected: rejectionTask,
})
})
})
// mark
return nextPromise
}
// test
const p = new MyPromise((resolve, reject) => {
setTimeout(() => resolve('resolved!'), 1000)
})
p.then((res) => {
return res + '!!'
}).then((res) => {
console.log(res)
})
console.log('global')
允许.then回调函数返回 Promise
有的时候我们会希望回调函数返回一个 Promise,并当它 resolved 之后再触发之后 then 链中的函数回调,比如我们需要先发起一个请求获取一个 Id,再根据返回的 Id 请求其他数据。如:
const follower = fetch('/followers')
const p = follower.then((ids) => {
const user = fetch(`/api/user/${id[0]}`)
return user
})
此时,除follower外存在着两个 Promise,一个是user ,一个是.then方法需要返回并赋值给 p 的 Promise。它们的状态和终值应该是相互锁定的,即当第一个 Promise 状态改变时,第二个 Promise 也随之发生相同变化。因此我们需要扩展一下我们的fulfill方法
class MyPromise {
constructor(fn) {
this.#state = 'pending'
this.#value = null
this.#handlers = []
// mark
fn(this.#resolve, this.#reject)
}
// mark
#resolve = (value) => {
if (isPromise(value)) {
value.then(
(val) => {
this.#resolve(val)
},
(err) => {
this.#reject(err)
}
)
} else {
this.#fulfill(value)
}
}
}
让我们来捋一捋当user的这个 Promise 从服务器得到数据,状态改变前后都发生了什么:
- 调用
.then,将 fullfillmentTask 加入微任务队列, 返回新 Promise p。 - 浏览器执行完主函数,开始执行微任务 fullfillmentTask。假设此时
follower已完成,则立即执行.then传入的回调函数(ids) => { return fetch(`/api/user/${id[0]}`) },并将返回值传给 p 的resolve方法执行 - 由于返回值是一个 Promise,于是给它挂上回调函数,来监控其状态变化。注意此时 this 的上下文为 p
- 某一时刻,
userfetch 从服务器得到数据 - 执行 3 中挂上的回调
(val) => { this.#revolve(val) }, 调用 p 的this.#revolve - 由于 p 没有后续的 then 链,所以无后续回调函数需要执行,p 内部状态变为
fulfilled, 终值为user的终值
Note
对于实际的 Promise 来说,返回值不仅可以是 Promise 对象,还可以是更广泛意义上的 Thenable Object,即任何带有then方法的对象
function isThenable(value) {
return typeof value?.then === 'function'
}
异常的集中处理
Promise 相比之前的一大优势,就是不再需要对每一步异步操作添加异常处理。只需要在 then 链的末尾添加就可以集中处理整个链上的异常
另外,由于在内部 Promise 会调用外部的函数,所以在使用的过程中应该进行一些保护,如异常处理。同时对于resolve和reject,最多只允许其中之一执行一次
class MyPromise {
constructor(fn) {
this.#state = 'pending'
this.#value = null
this.#handlers = []
// mark
this.#safeRun(fn, this.#resolve, this.#reject)
}
#safeRun = (fn, onFulfilled, onRejected) => {
let done
try {
fn(
(val) => {
if (done) {
return
}
done = true
onFulfilled(val)
},
(val) => {
if (done) {
return
}
done = true
onRejected(val)
}
)
} catch (err) {
if (done) {
return
}
done = true
onRejected(err)
}
}
#resolve = (value) => {
if (isThenable(value)) {
// mark
this.#safeRun(value.then, this.#resolve, this.#reject)
} else {
this.#fulfill(value)
}
}
}
总结
到此,我们实现一个 Promise 所需的核心功能,完整代码请参见gist。