Shio Y. Blog

从 0 实现一个 Promise

本文旨在通过实现一个 Promise 类来深入理解 Promise 的运行机制,虽然会提到一些 Promise 的基本概念,但最好先有一定了解, 可通过 MDN 进行查询

术语

由于对 Reolve、Fulfill、Reject 之类的术语并没有十分统一的中文译名,本文将保留其英文名

Promise 的本质是一个状态机

Promise 总共具有三个状态:Pending、Fulfilled、Rejected。在创建 Promise 对象时,会传入一个由开发者定义的函数 fn。开发者在其中自由的编写业务逻辑,可以是文件读取、网络请求,并通过调用 fn 的两个参数来控制 Promise 改变状态的实际。 Promise 本质就是一个这样的辅助工具,让开发者专注于业务逻辑。

我们可以在 Chrome Devtool 中 print promise 对象来查看它当前的状态和终值(Eventual Value)


_10
Promise.resolve(1)
_10
> Promise {<fulfilled>: 1}

所以实现 Promise 第一步就是实现一个状态机,定义这些状态,与转换状态的方法


_21
class MyPromise {
_21
#state
_21
#val // promise
_21
_21
constructor(fn) {
_21
this.#state = 'pending'
_21
this.val = null
_21
_21
fn(this.#fulfill, this.#reject)
_21
}
_21
_21
#fulfill = (value) => {
_21
this.#state = 'fulfilled'
_21
this.val = value
_21
}
_21
_21
#reject = (value) => {
_21
this.#state = 'rejected'
_21
this.val = value
_21
}
_21
}

.then 函数

Promise 允许开发者在任意时间添加对 Promise 状态改变的监听,通过调用.then(onFullfilled, OnRejected)并传入两个回调函数实现。

注意开发者可多次调用.then()函数,如:


_10
const p = Promise.resolve('resolved')
_10
p.then((v) => console.log(v))
_10
p.then((v) => console.log('Yeah!'))
_10
_10
> resolved
_10
> Yeah!

因此我们需要就这些 Handler 一次存储起来,在状态改变时依次调用。


_62
function addToTaskQueue(fn) {
_62
// 模拟浏览器微任务
_62
setTimeout(fn, 0)
_62
}
_62
_62
class MyPromise {
_62
#state
_62
#value // promise
_62
#handlers = []
_62
_62
constructor(fn) {
_62
this.#state = 'pending'
_62
this.#value = null
_62
_62
fn(this.#fulfill, this.#reject)
_62
}
_62
_62
#fulfill = (value) => {
_62
this.#state = 'fulfilled'
_62
this.#value = value
_62
this.#handlers.forEach((handler) => {
_62
handler.onFulfilled(this.#val)
_62
})
_62
this.#handlers = null // garbage collecting
_62
}
_62
_62
#reject = (value) => {
_62
this.#state = 'rejected'
_62
this.#value = value
_62
this.#handlers.forEach((handler) => {
_62
handler.onRejected(this.#val)
_62
})
_62
this.#handlers = null // garbage collecting
_62
}
_62
then = (onFulfilled, onRejected) => {
_62
addToTaskQueue(() => {
_62
if (this.#state === 'pending') {
_62
this.#handlers.push({ onFulfilled, onRejected })
_62
}
_62
if (this.#state === 'fulfilled') {
_62
onFulfilled(this.#value)
_62
}
_62
if (this.#state === 'rejected') {
_62
onRejected(this.#value)
_62
}
_62
})
_62
}
_62
}
_62
_62
// Test Code
_62
const p = new MyPromise((resolve, reject) => {
_62
setTimeout(() => resolve('resolved!'), 1000)
_62
})
_62
p.then(
_62
(res) => {
_62
console.log(res)
_62
},
_62
(err) => {
_62
console.log(err)
_62
}
_62
)
_62
console.log('global')

由于 Promise 状态的改变可能在调用then之前或之后,所以需要在多处进行判断。

在进行下一步之前,对代码的通用部分进行复用,添加类型检查:


_47
class MyPromise {
_47
#state
_47
#value
_47
#handlers
_47
_47
constructor(fn) {
_47
this.#state = 'pending'
_47
this.#value = null
_47
this.#handlers = []
_47
fn(this.#fulfill, this.#reject)
_47
}
_47
_47
#fulfill = (value) => {
_47
this.#state = 'fulfilled'
_47
this.#value = value
_47
this.#handlers.forEach(this.#handle)
_47
this.#handlers = null // garbage collecting
_47
}
_47
_47
#reject = (err) => {
_47
this.#state = 'rejected'
_47
this.#value = err
_47
this.#handlers.forEach(this.#handle)
_47
this.#handlers = null // garbage collecting
_47
}
_47
_47
#handle = (handler) => {
_47
if (this.#state === 'pending') {
_47
this.#handlers.push(handler)
_47
}
_47
if (this.#state === 'fulfilled') {
_47
handler.onFulfilled(this.#value)
_47
}
_47
if (this.#state === 'rejected') {
_47
handler.onRejected(this.#value)
_47
}
_47
}
_47
_47
then = (onFulfilled, onRejected) => {
_47
addToTaskQueue(() => {
_47
this.#handle({
_47
onFulfilled,
_47
onRejected,
_47
})
_47
})
_47
}
_47
}

.then的链式调用

Promise 的一个重要特性,就是允许链式的调用,从而一定程度上避免了回调地狱的发生。为了做到这一点,我们需要将回调函数 onFulfilled/OnRejected 的返回值穿透到最外层,即创建并返回一个以回调函数返回值为内部状态的 Promise,以便进一步调用后续的.then方法。


_34
then = (onFulfilled, onRejected) => {
_34
const nextPromise = new MyPromise((resolve, reject) => {
_34
const fullfillmentTask = () => {
_34
const value =
_34
typeof onFulfilled === 'function' ? onFulfilled(this.#value) : this.#value
_34
resolve(value)
_34
}
_34
const rejectionTask = () => {
_34
const value =
_34
typeof onRejected === 'function' ? onRejected(this.#value) : this.#value
_34
resolve(value)
_34
}
_34
_34
addToTaskQueue(() => {
_34
this.#handle({
_34
onFulfilled: fullfillmentTask,
_34
onRejected: rejectionTask,
_34
})
_34
})
_34
})
_34
_34
return nextPromise
_34
}
_34
_34
// test
_34
const p = new MyPromise((resolve, reject) => {
_34
setTimeout(() => resolve('resolved!'), 1000)
_34
})
_34
p.then((res) => {
_34
return res + '!!'
_34
}).then((res) => {
_34
console.log(res)
_34
})
_34
console.log('global')

允许.then回调函数返回 Promise

有的时候我们会希望回调函数返回一个 Promise,并当它 resolved 之后再触发之后 then 链中的函数回调,比如我们需要先发起一个请求获取一个 Id,再根据返回的 Id 请求其他数据。如:


_10
const follower = fetch('/followers')
_10
const p = follower.then((ids) => {
_10
const user = fetch(`/api/user/${id[0]}`)
_10
return user
_10
})

此时,除follower外存在着两个 Promise,一个是user ,一个是.then方法需要返回并赋值给 p 的 Promise。它们的状态和终值应该是相互锁定的,即当第一个 Promise 状态改变时,第二个 Promise 也随之发生相同变化。因此我们需要扩展一下我们的fulfill方法


_23
class MyPromise {
_23
constructor(fn) {
_23
this.#state = 'pending'
_23
this.#value = null
_23
this.#handlers = []
_23
fn(this.#resolve, this.#reject)
_23
}
_23
_23
#resolve = (value) => {
_23
if (isPromise(value)) {
_23
value.then(
_23
(val) => {
_23
this.#resolve(val)
_23
},
_23
(err) => {
_23
this.#reject(err)
_23
}
_23
)
_23
} else {
_23
this.#fulfill(value)
_23
}
_23
}
_23
}

让我们来捋一捋当user的这个 Promise 从服务器得到数据,状态改变前后都发生了什么:

  1. 调用.then,将 fullfillmentTask 加入微任务队列, 返回新 Promise p。
  2. 浏览器执行完主函数,开始执行微任务 fullfillmentTask。假设此时follower已完成,则立即执行.then传入的回调函数(ids) => { return fetch(`/api/user/${id[0]}`) } ,并将返回值传给 p 的resolve方法执行
  3. 由于返回值是一个 Promise,于是给它挂上回调函数,来监控其状态变化。注意此时 this 的上下文为 p
  4. 某一时刻,user fetch 从服务器得到数据
  5. 执行 3 中挂上的回调 (val) => { this.#revolve(val) }, 调用 p 的this.#revolve
  6. 由于 p 没有后续的 then 链,所以无后续回调函数需要执行,p 内部状态变为fulfilled, 终值为 user的终值

Note

对于实际的 Promise 来说,返回值不仅可以是 Promise 对象,还可以是更广泛意义上的 Thenable Object,即任何带有then方法的对象


_10
function isThenable(value) {
_10
return typeof value?.then === 'function'
_10
}

异常的集中处理

Promise 相比之前的一大优势,就是不再需要对每一步异步操作添加异常处理。只需要在 then 链的末尾添加就可以集中处理整个链上的异常

另外,由于在内部 Promise 会调用外部的函数,所以在使用的过程中应该进行一些保护,如异常处理。同时对于resolvereject,最多只允许其中之一执行一次


_44
class MyPromise {
_44
constructor(fn) {
_44
this.#state = 'pending'
_44
this.#value = null
_44
this.#handlers = []
_44
this.#safeRun(fn, this.#resolve, this.#reject)
_44
}
_44
_44
#safeRun = (fn, onFulfilled, onRejected) => {
_44
let done
_44
try {
_44
fn(
_44
(val) => {
_44
if (done) {
_44
return
_44
}
_44
done = true
_44
onFulfilled(val)
_44
},
_44
(val) => {
_44
if (done) {
_44
return
_44
}
_44
done = true
_44
onRejected(val)
_44
}
_44
)
_44
} catch (err) {
_44
if (done) {
_44
return
_44
}
_44
done = true
_44
onRejected(err)
_44
}
_44
}
_44
_44
#resolve = (value) => {
_44
if (isThenable(value)) {
_44
this.#safeRun(value.then, this.#resolve, this.#reject)
_44
} else {
_44
this.#fulfill(value)
_44
}
_44
}
_44
}

总结

到此,我们实现一个 Promise 所需的核心功能,完整代码请参见gist

推荐阅读

写于 2022年07月29日