外观
怎么手写一个 Promise?
手写版要有三个状态、不可逆的状态转移,以及返回新 Promise 的 then。回调放进微任务。完整规范还要求:resolve 一个 thenable 时,跟随它的结果,而不是把它当成普通成功值。
原理
下面这份实现覆盖状态、链式调用和错误传递。它故意没有解开 thenable,方便先把主流程讲清楚。
js
const PENDING = "pending"
const FULFILLED = "fulfilled"
const REJECTED = "rejected"
class MyPromise {
constructor(executor) {
this.state = PENDING
this.value = undefined
this.handlers = []
const resolve = (value) => settle(this, FULFILLED, value)
const reject = (reason) => settle(this, REJECTED, reason)
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this.handlers.push({ onFulfilled, onRejected, resolve, reject })
if (this.state !== PENDING) flush(this)
})
}
}
function settle(promise, state, value) {
if (promise.state !== PENDING) return
promise.state = state
promise.value = value
flush(promise)
}
function flush(promise) {
queueMicrotask(() => {
for (const handler of promise.handlers.splice(0)) {
const cb =
promise.state === FULFILLED ? handler.onFulfilled : handler.onRejected
try {
if (typeof cb !== "function") {
const forward = promise.state === FULFILLED ? handler.resolve : handler.reject
forward(promise.value)
} else {
handler.resolve(cb(promise.value))
}
} catch (err) {
handler.reject(err)
}
}
})
}then 缺少某个回调时,成功值或失败原因原样传给下一环,这样错误不会在中间丢掉。回调抛错时,下一环失败。
规范里的 Promise 解析过程是:如果 resolve 收到的值有 then 方法,就调用它,并等它成功或失败后再结算当前 Promise;如果这个值就是当前 Promise 自己,直接拒绝,避免循环。手写题讲到这里,再补代码即可。
Promise.all 的思路:计数还没完成的个数,有一个拒绝就立刻拒绝,全部完成才用按下标放好的结果数组去解决。
追问
为什么回调必须是微任务?
即使 Promise 已经完成,then 也要异步调用,这样同步代码的顺序是稳定的,也不会在执行器里递归地同步触发整条链。见 Event Loop。
catch 怎么来?
catch(onRejected) 就是 then(undefined, onRejected)。finally 不改变链上的值,只保证一段清理一定会执行。
易错点
resolve两次时,第二次要被忽略。then必须返回新 Promise。返回自己会让链无法表示「下一步」。- 解开 thenable 时要防止
then被调用两次,也要防止值是自己。