手写 Promise,深入理解其核心原理
- 为什么
executor会立即执行? - 为什么
Promise的状态只能改变一次? - 为什么
throw error会让 Promise 变成失败状态? - 为什么
then可以拿到成功或失败的结果? - 为什么
then可以链式调用?
通过手写一个 MyPromise,一步一步拆解 Promise 的核心原理。
基础结构
1. 从 Promise 的使用方式开始
先回顾一下 Promise 最常见的创建方式:
new Promise((resolve, reject) => {
// 执行异步或同步任务
})
从这个使用方式中可以看出几个关键信息:
- Promise 可以通过
new调用,所以它可以是一个类 - 构造函数接收一个函数作为参数,这个函数通常叫做
executor executor会在创建 Promise 时立即执行executor接收两个参数:resolve和rejectresolve用来把任务标记为成功reject用来把任务标记为失败
所以我们可以先搭建出 MyPromise 的基本结构。
2. 搭建 MyPromise 的基本结构
这一节先不考虑状态变化,也不实现 then 方法,只模拟最基础的调用形式:
new MyPromise((resolve, reject) => {
// executor 会立即执行
})
因此,我们需要定义一个 MyPromise 类,并在构造函数中立即调用 executor。
class MyPromise {
constructor(executor) {
executor(this._resolve, this._reject)
}
/**
* 将任务标记为完成
* @param data 完成时的数据,例如 resolve([1, 2])
*/
_resolve(data) {
}
/**
* 将任务标记为失败
* @param reason 任务失败的原因
*/
_reject(reason) {
}
}
现在 executor 已经可以立即执行了,但是还没有记录任务状态,也没有保存成功或失败的结果。
3. 增加状态和值
Promise 内部有三种状态:
pending:初始状态fulfilled:成功状态rejected:失败状态
状态的流转规则也很简单:
- 可以从
pending变成fulfilled - 可以从
pending变成rejected - 一旦变成
fulfilled或rejected,状态就不能再次改变
除了状态之外,Promise 还需要保存成功结果或失败原因。
例如:
resolve(1)
reject(new Error('fail'))
这些值都需要保存下来,后续传给 then 的回调。
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
/**
* 任务执行器
* @param executor
*/
constructor(executor) {
this._state = PENDING
this._value = undefined
executor(this._resolve.bind(this), this._reject.bind(this))
}
/**
* 更新当前状态
* @param newState 新状态
* @param value 成功结果或失败原因
*/
_changeState(newState, value) {
if (this._state !== PENDING)
return
this._state = newState
this._value = value
}
/**
* 将任务标记为完成
* @param data 完成时的数据,例如 resolve([1, 2])
*/
_resolve(data) {
this._changeState(FULFILLED, data)
}
/**
* 将任务标记为失败
* @param reason 任务失败的原因
*/
_reject(reason) {
this._changeState(REJECTED, reason)
}
}
这里的 _changeState 是状态切换的统一入口。
它有两个作用:
- 修改当前 Promise 的状态
- 保存成功结果或失败原因
其中最重要的是这段判断:
if (this._state !== PENDING)
return
它保证了 Promise 的状态只能从 pending 改变一次。
4. 保证状态只能改变一次
Promise 的状态一旦确定,就不能再次改变。
例如:
const p = new MyPromise((resolve, reject) => {
resolve('success')
reject('error')
})
console.log(p)
虽然 executor 中同时调用了 resolve 和 reject,但第一次调用 resolve 时,状态已经从 pending 变成了 fulfilled。
所以后面的 reject 会被忽略。
也就是说,当前 Promise 最终保存的状态和值应该是:
{
_state: 'fulfilled',
_value: 'success'
}
这也是 _changeState 中判断当前状态是否为 pending 的原因。
5. 捕获 executor 中的异常
原生 Promise 中,如果 executor 执行过程中抛出异常,Promise 会自动变成 rejected 状态。
例如:
const p = new Promise(() => {
throw new Error('error')
})
所以我们需要在执行 executor 时使用 try...catch 包裹。
class MyPromise {
constructor(executor) {
this._state = PENDING
this._value = undefined
try {
executor(this._resolve.bind(this), this._reject.bind(this))
}
catch (error) {
this._reject(error)
}
}
}
这样,当 executor 中出现异常时,就会自动调用 _reject,把当前 Promise 标记为失败。
then 方法
then 是 Promise 中最核心的方法,它的作用是注册成功和失败后的回调。
它接收两个参数:
onFulfilled:Promise 变成fulfilled后执行onRejected:Promise 变成rejected后执行
我们使用是这样的:
const p = new Promise((resolve, reject) => {
resolve(123)
})
p.then(
(data) => {
console.log('fulfilled:', data)
},
(reason) => {
console.log('rejected:', reason)
}
)
1. then 会返回一个新的 Promise
原生 Promise 中,then 方法一定会返回一个新的 Promise。
这也是 Promise 可以链式调用的原因:
promise
.then((value) => {
return value + 1
})
.then((value) => {
console.log(value)
})
所以,MyPromise 的 then 方法也需要返回一个新的 MyPromise。
class MyPromise {
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
})
}
}
2. pending 状态下需要保存回调
如果调用 then 时,Promise 已经是 fulfilled 或 rejected 状态,那么可以直接执行对应的回调。
但更多时候,调用 then 时 Promise 还处于 pending 状态。
比如:
const p = new MyPromise((resolve) => {
setTimeout(() => {
resolve(123)
}, 2000)
})
p.then((data) => {
console.log(data)
})
调用 then 的时候,setTimeout 还没有执行,Promise 仍然是 pending。
所以此时不能马上执行 onFulfilled,而是要先把它保存起来。
等后续调用 resolve 改变状态后,再统一执行这些回调。
因此,我们需要在 MyPromise 中增加一个队列,用来保存通过 then 注册的任务。
class MyPromise {
constructor(executor) {
this._state = PENDING
this._value = undefined
this._handlers = []
try {
executor(this._resolve.bind(this), this._reject.bind(this))
}
catch (error) {
this._reject(error)
}
}
}
3. 保存 then 注册的回调任务
每次调用 then,其实都是注册两个可能执行的任务:
- Promise 成功时执行
onFulfilled - Promise 失败时执行
onRejected
同时,因为 then 返回了一个新的 Promise,所以我们还需要保存这个新 Promise 的 resolve 和 reject。
这样后面执行回调时,才能决定让 then 返回的新 Promise 成功还是失败。
class MyPromise {
constructor(executor) {
this._state = PENDING
this._value = undefined
this._handlers = []
try {
executor(this._resolve.bind(this), this._reject.bind(this))
}
catch (error) {
this._reject(error)
}
}
/**
* 注册成功和失败回调
* @param onFulfilled 成功回调
* @param onRejected 失败回调
*/
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this._pushHandler(onFulfilled, FULFILLED, resolve, reject)
this._pushHandler(onRejected, REJECTED, resolve, reject)
})
}
/**
* 保存 then 注册的回调任务
* @param executor 当前需要执行的回调
* @param state 这个回调在哪种状态下执行
* @param resolve then 返回的新 Promise 的 resolve
* @param reject then 返回的新 Promise 的 reject
*/
_pushHandler(executor, state, resolve, reject) {
this._handlers.push({ executor, state, resolve, reject })
}
}
到这里,我们只是把 then 注册的回调保存起来了,还没有真正执行它们。
下一步需要在 Promise 状态改变后,从 _handlers 中找到对应状态的任务并执行。
4. 确定回调的执行时机
前面我们已经把 then 注册的回调保存到了 _handlers 队列中。
接下来要解决的问题是:什么时候执行这些回调?
有两个时机需要触发队列执行。
第一个时机是:Promise 状态发生变化时。
也就是从 pending 变成 fulfilled 或 rejected 时,需要尝试执行队列中的任务。
例如:
const p = new MyPromise((resolve) => {
setTimeout(() => {
resolve(123)
}, 2000)
})
p.then((data) => {
console.log(data)
})
调用 then 时,Promise 还是 pending,所以回调会先进入 _handlers 队列。
等到 resolve(123) 执行时,Promise 状态变成 fulfilled,这时就需要执行队列中的回调。
因此,我们需要在 _changeState 中调用 _runHandlers。
class MyPromise {
_changeState(newState, value) {
if (this._state !== PENDING)
return
this._state = newState
this._value = value
this._runHandlers()
}
_runHandlers() {
if (this._state === PENDING)
return
}
}
_runHandlers 中先判断当前状态是否仍然是 pending。
如果还是 pending,说明结果还没有产生,回调不能执行,直接返回。
第二个时机是:Promise 状态已经确定之后,再调用 then。
例如:
const p = new MyPromise((resolve) => {
resolve(1)
})
p.then((value) => {
console.log(value)
})
这个例子中,resolve(1) 是同步执行的。
也就是说,当后面调用 then 时,Promise 已经是 fulfilled 状态了。
如果只在 _changeState 中调用 _runHandlers,这个 then 注册的回调就不会被执行。
所以,每次调用 then 保存完回调后,也需要再调用一次 _runHandlers。
class MyPromise {
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this._pushHandler(onFulfilled, FULFILLED, resolve, reject)
this._pushHandler(onRejected, REJECTED, resolve, reject)
this._runHandlers()
})
}
}
5. 将 then 回调放入微任务队列
原生 Promise 中,then 的回调不会立即执行。
例如:
Promise.resolve().then(() => {
console.log('then')
})
console.log('end')
输出结果是:
end
then
这说明 then 注册的回调会等当前同步代码执行完后,再进入微任务队列执行。
为了模拟这个行为,我们可以封装一个 runMicroTask 方法,用来把回调放入微任务队列。
function runMicroTask(cb) {
if (typeof process !== 'undefined' && typeof process.nextTick === 'function') {
process.nextTick(cb)
}
else if (typeof MutationObserver !== 'undefined') {
const p = document.createElement('p')
const observer = new MutationObserver(cb)
observer.observe(p, { childList: true })
p.innerHTML = '1'
}
else {
setTimeout(cb, 0)
}
}
这里的处理顺序是:
- 如果是 Node.js 环境,优先使用
process.nextTick - 如果是浏览器环境,优先使用
MutationObserver - 如果以上方式都不可用,则降级为
setTimeout
6. 执行队列中的任务
确定执行时机之后,就可以开始处理 _handlers 队列了。
队列中保存的每一项大概长这样:
{
executor: onFulfilled,
state: 'fulfilled',
resolve,
reject
}
其中:
executor是当前需要执行的回调state表示这个回调在哪种状态下执行resolve用来让then返回的新 Promise 成功reject用来让then返回的新 Promise 失败
所以执行队列时,需要做几件事:
- 如果当前 Promise 还是
pending,不执行 - 从
_handlers中取出任务 - 判断任务的
state是否和当前 Promise 状态一致 - 如果状态一致,就把任务放入微任务队列执行
- 执行完成后,从队列中删除这个任务
class MyPromise {
_runHandlers() {
if (this._state === PENDING)
return
while (this._handlers[0]) {
this._runHandlerItem(this._handlers[0])
this._handlers.shift()
}
}
_runHandlerItem(handler) {
runMicroTask(() => {
const { executor, state, resolve, reject } = handler
if (this._state !== state)
return
// 后面继续处理 executor 的执行逻辑
})
}
}
这里先把队列执行流程搭出来。
真正执行单个任务时,还需要继续处理几个问题:
then参数不是函数怎么办?- 回调正常返回普通值怎么办?
- 回调返回 Promise 怎么办?
- 回调执行过程中抛出异常怎么办?
7. 处理 then 参数不是函数的情况
then 的两个参数都是可选的。
例如:
promise
.then()
.then((value) => {
console.log(value)
})
第一个 then 没有传入任何回调,但成功的值仍然需要继续向后传递。
这叫做值穿透。
失败状态也是一样的:
promise
.then(null)
.then(null, (reason) => {
console.log(reason)
})
第一个 then 没有处理失败原因,所以失败原因也需要继续向后传递。
这叫做异常穿透。
所以如果 executor 不是函数,就不能直接调用,而是根据当前 Promise 的状态继续传递结果。
if (typeof executor !== 'function') {
this._state === FULFILLED
? resolve(this._value)
: reject(this._value)
return
}
8. 处理回调返回 Promise 的情况
如果 executor 是函数,就可以正常执行它。
最简单的情况是回调返回一个普通值:
promise
.then((value) => {
return value + 1
})
.then((value) => {
console.log(value)
})
这时只需要把回调的返回值作为 then 返回的新 Promise 的成功结果。
const result = executor(this._value)
resolve(result)
但是,then 回调也可能返回一个 Promise。
例如:
const p = new MyPromise((resolve) => {
resolve(1)
})
p.then((value) => {
return new MyPromise((resolve) => {
resolve(value + 1)
})
}).then((value) => {
console.log(value)
})
第一个 then 的回调返回了一个新的 Promise。
这时不能直接把这个 Promise 对象作为结果传给下一个 then,而是要等待它的状态确定:
- 如果它成功,就让当前
then返回的新 Promise 成功 - 如果它失败,就让当前
then返回的新 Promise 失败
所以需要这样处理:
const result = executor(this._value)
if (isPromise(result)) {
result.then(resolve, reject)
}
else {
resolve(result)
}
这里的 isPromise 用来判断一个值是否可以当成 Promise 处理。
为了方便,我们先用是否存在 then 方法来判断:
function isPromise(obj) {
return !!(obj && typeof obj.then === 'function')
}
最后,把这些逻辑合在一起,_runHandlerItem 可以写成这样:
class MyPromise {
_runHandlerItem(handler) {
runMicroTask(() => {
const { executor, state, resolve, reject } = handler
if (this._state !== state)
return
if (typeof executor !== 'function') {
this._state === FULFILLED
? resolve(this._value)
: reject(this._value)
return
}
try {
const result = executor(this._value)
if (isPromise(result)) {
result.then(resolve, reject)
}
else {
resolve(result)
}
}
catch (error) {
reject(error)
}
})
}
}
这样就可以同时处理:
- 回调返回普通值
- 回调返回 Promise
- 回调执行过程中抛出异常
9. 当前 then 实现小结
截止这里,then 已经具备了几个核心能力:
- 可以注册成功和失败回调
- 可以在
pending状态下先保存回调 - 可以在状态改变后执行对应回调
- 可以保证回调在微任务中执行
- 可以处理
then参数不是函数时的值穿透和异常穿透 - 可以处理回调返回普通值
- 可以处理回调返回 Promise
- 可以捕获回调执行过程中的异常
实例方法
1. catch 方法
catch 用来注册失败回调。
它本质上就是只传入失败回调的 then:
promise.then(null, onRejected)
所以实现非常简单:
class MyPromise {
catch(onRejected) {
return this.then(null, onRejected)
}
}
2. finally 方法
finally 的特点是:无论 Promise 最终成功还是失败,都会执行传入的回调。
并且基础情况下,finally 不会改变原来的结果。
例如:
MyPromise.resolve(1)
.finally(() => {
console.log('finally')
})
.then((value) => {
console.log(value)
})
最终 then 里拿到的还是 1。
基础实现如下:
class MyPromise {
finally(onSettled) {
return this.then(
(data) => {
onSettled()
return data
},
(reason) => {
onSettled()
throw reason
}
)
}
}
静态方法
1. MyPromise.resolve
MyPromise.resolve 用来返回一个成功状态的 Promise。
最简单的情况是传入普通值:
MyPromise.resolve(1)
等价于:
new MyPromise((resolve) => {
resolve(1)
})
不过它还有两个特殊情况:
- 如果传入的本身就是
MyPromise,直接返回它 - 如果传入的是 Promise-like 对象,需要等待它的状态
class MyPromise {
static resolve(data) {
if (data instanceof MyPromise) {
return data
}
return new MyPromise((resolve, reject) => {
if (isPromise(data)) {
data.then(resolve, reject)
}
else {
resolve(data)
}
})
}
}
2. MyPromise.reject
MyPromise.reject 用来返回一个失败状态的 Promise。
class MyPromise {
static reject(reason) {
return new MyPromise((resolve, reject) => {
reject(reason)
})
}
}
3. MyPromise.all
MyPromise.all 接收一个可迭代对象,返回一个新的 Promise。
它的规则是:
- 所有任务都成功,最终才成功
- 最终成功的结果是一个数组
- 结果数组的顺序和传入顺序一致
- 只要有一个任务失败,最终就失败
例如:
MyPromise.all([
MyPromise.resolve(1),
MyPromise.resolve(2),
3
]).then((result) => {
console.log(result)
})
输出:
[1, 2, 3]
实现时需要注意两点:
- 每一项都要用
MyPromise.resolve包一下,因为传入的可能是普通值 - 结果要按照原始顺序保存,而不是按照完成顺序保存
class MyPromise {
static all(proms) {
return new MyPromise((resolve, reject) => {
try {
const results = []
let count = 0
let fulfilledCount = 0
for (const p of proms) {
const i = count
count++
MyPromise.resolve(p).then((data) => {
fulfilledCount++
results[i] = data
if (fulfilledCount === count) {
resolve(results)
}
}, reject)
}
if (count === 0) {
resolve(results)
}
}
catch (error) {
reject(error)
}
})
}
}
这里的 i 用来记录当前 Promise 在原始数组中的位置。
即使后面的 Promise 先完成,结果也会放回原来的位置。