我把 JS Promise 翻了个底朝天,以后再也不怕 Promise 面试题了
开头
JS Promise
的源码使用了多个设计模式,比如工厂、观察者等。其中,我认为最核心的是 观察者模式
,executor 中的代码类似被观察者
,通过 then
传入的回调函数类似观察者
,被观察者
通过 resolve/reject
方法通知到观察者
。
Promise
的实现并不是完全符合观察者模式
的定义。在观察者模式
中,观察者
必须要显式地注册到被观察者
中,而在 Promise
中,then
方法中的回调函数并不需要显式地注册到 Promise
实例中,而是通过调用 then
方法来间接地与 Promise
实例关联起来。此外,Promise
还有一些其他的特性,如 Promise.all
和 Promise.race
等,这些特性在观察者模式
中并不存在。
MDN 官网对 Promise 的特性给出了不少的规则描述,而要彻底掌握 Promise,最简单的方法就是实现一个 Promise,我们开始吧!
制作自己的 Promise
大概框架
先写出大概的框架
class MyPromise {
constructor() {}
private resolve = () => {};
private reject = () => {};
then = () => {};
catch = () => {};
finally = () => {};
}
初始化状态和结果
Promise 内部维护了一个状态机。有三种状态:
- pending
- resolved
- rejected
状态的扭转很简单,只有两种情况:
- pending → resolved,拿到成功的数据
- pending → rejected,拿到失败的原因
初始化状态和结果
enum State {
Pending,
Resolved,
Rejected,
}
class MyPromise {
// 初始状态为 pending
private state = State.Pending;
// 保存 Promise 的结果
private result?: any;
//..
}
状态的扭转
下图可以看出,用 resolve/reject 方法来扭转状态,并且状态和结果是强绑定的。
添加 resolve/reject 方法,更新状态和结果
从 setResultAndStatus
方法可以看出,如果是 pending 状态的话,resolve/reject
方法不做任何操作,所以 Promise 的状态扭转是不可逆的
class MyPromise {
//..
private resolve = (value: any | MyPromise) => {
this.setResultAndStatus(value, State.Resolved);
};
private reject = (reason?: any) => {
this.setResultAndStatus(reason, State.Rejected);
};
private setResultAndStatus = (value: any, state: State) => {
// 这里就是为什么 Promise 的状态是不可逆的. 如果 status 不是 pending 则是直接 return
if (this.state !== State.Pending) {
return;
}
this.state = state;
this.result = value;
};
}
executor - the Revealing Constructor Pattern
我们通过 new Promise
的时候通过构造函数给到 Promise executor 的具体实现
这种通过构造函数暴露内部方法的设计,用到了 revealing constructor pattern
设计模式。好处是,只有 new Promise
时外界可以操作 Promise 的状态。
补充构造函数的代码
class MyPromise {
//..
constructor(
executor: (
resolve: (value: any | MyPromise) => void,
reject: (reason?: any) => void
) => void
) {
executor(this.resolve, this.reject);
}
//..
}
executor 抛出异常
添加 try catch
逻辑,如果 executor
中抛出异常,我们通过 .then
或者 .catch
传入的处理异常的回调函数会被调用
class MyPromise {
//..
constructor(
//..
) {
try {
executor(this.resolve, this.reject);
} catch (e) {
this.reject(e); // 这样抛出异常的话下面的 .catch 可以接到
}
}
//..
}
executor 支持异步调用
executor 是同步代码,但它的内部可以通过 setTimeout
等方法进行异步操作
- 新增
handlersQueue
队列用来存放通过 then 方法传入的回调函数。 - 新增
executeHandlers
用来执行队列中的回调函数。 then
中的回调会先加入队列,等executor
中的异步代码跑完,通过resolve
方法调用到executeHandlers
class MyPromise {
// 它的作用下面会讲到
private handlersQueue: Handler[] = [];
private setResultAndStatus = (value: any, state: State) => {
//..
// 扭转完状态后,处理所有回调函数
this.executeHandlers();
};
private executeHandlers = () => {
// 如果状态还是 pending,说明 executor 中的异步代码还没有结束。直接返回,不执行, 不执行 handlers 是没关系的,因为 handlers 都保存在 handlersQueue 中,等待着状态改变后会再执行的
if (this.state === State.Pending) {
return;
}
// 处理所有回调函数
this.handlersQueue.forEach((handler) => {
if (this.state === State.Resolved) {
handler.handleOnFullfilled(this.result); // 此时 result 是异步操作成功后给的数据
} else {
handler.handleOnRejected(this.result); // 此时 result 是异步操作失败后给的失败原因
}
});
// 执行完毕后,清空 handlers
this.handlersQueue = [];
};
}
then
值穿透和链式调用的原理都在 then
方法中。
then
方法的原理可以总结为:注册回调函数,等待 Promise
对象的状态扭转后,构造一个新的Promise
对象并返回它,然后下一层
可以继续调用 then
。
then 和微任务
then 中的回调函数会在微任务阶段执行,实现起来很简单,把执行回调函数的代码放入微任务队列即可
const runAsync = (cb: () => void) => {
setTimeout(cb, 0);
};
class MyPromise {
// ..
private executeHandlers = () => {
// ..
// 模拟放入微任务队列,这样 then 中的回调会在微任务阶段被调用
runAsync(() => {
this.handlersQueue.forEach((handler) => {
if (this.state === State.Resolved) {
handler.handleOnFullfilled(this.result);
} else {
handler.handleOnRejected(this.result);
}
});
this.handlersQueue = [];
});
};
then = (
onFullfilled?: (value: any) => any | MyPromise,
onRejected?: (reason: any) => any | MyPromise
) => {
// ..
// 执行队列中的所有回调函数
this.executeHandlers();
// ..
};
}
then 和值穿透
值穿透使得我们可以在 then 方法中省略不需要的回调函数,下面是值穿透的例子
new Promise((resolve, _) => {
resolve(1);
})
.then(2) // 2 不会往下传
.then(console.log); // 1
值穿透的原理见下方代码注释
executeHandlers = () => {
//..
runAsync(() => {
this.handlersQueue.forEach((handler) => {
// 根据状态执行不同的回调函数
if (this.state === State.Resolved) {
// 通过这行代码把本次 promise 的结果传递给下一个 promise
handler.handleOnFullfilled(this.result);
}
});
});
//..
};
then = (
onFullfilled?: (value: any) => any | MyPromise,
onRejected?: (reason: any) => any | MyPromise
) => {
return new MyPromise((resolve: any, reject: any) => {
const handler = {
handleOnFullfilled: (value: any) => {
if (!onFullfilled || typeof onFullfilled !== "function") {
// 这里可以看到值穿透的原理:
// 如果给 then 的不是个回调函数,那么新创建的 promise 的 result 直接设为 value
resolve(value);
} else {
// ..
}
},
// handleOnRejected 原理同上
});
};
then 和链式调用
我画了一个图来描述链式调用,then
返回新的 Promise
,它继续调用 then

由下方代码也可以看出 链式调用的原理是,then 返回一个新的 Promise,它可以继续调用下一个 then
then = (
onFullfilled?: (value: any) => any | MyPromise,
onRejected?: (reason: any) => any | MyPromise
) => {
// 返回一个新的 promise,这样可以实现链式调用,即 then 的返回值是个 Promise, 可以继续调用 then
return new MyPromise((resolve: any, reject: any) => {
//..
}
};
then 和并行执行
MDN 文档中有这么一段描述:
大体意思是,当我们在同一个 promise (取名 promise1) 上分别调用两次 then (不是链式调用)的时候,会创建两个新的 promise,分别放入到这个 promise1 中的队列。类似于开了两条道路,这两条道路互不干扰。就像下面这张图:

下面代码中,promise1
内部的队列会保存两次调用 then
生成的新 promise
,以确保它们可以按顺序调用
const promise1 = new Promise((resolve) => resolve("ok"));
// then 会生成一个新的 promise, 然后放入 promise1 内部的队列中
promise1.then((v) => {
return new Promise((resolve) => {console.log(1)});
});
// 此时 promise1 的 handlersQueue 的长度为1
// then 会又生成一个新的 promise, 然后又放入 promise1 内部的队列中
promise1.then((v) => {
return new Promise((resolve) => {console.log(2)});
});
// 此时 promise1 的 handlersQueue 的长度为2
// output:
// 1
// 2
那么怎么确保回调函数是按顺序调用的呢?这时就需要一个队列来存放回调函数们,也就是代码中的 handlersQueue
队列
代码实现:
class MyPromise {
// 用来保存多个 then (不是链式调用) 传入的回调函数
private handlersQueue: Handler[] = [];
//..
private executeHandlers = () => {
//..
runAsync(() => {
// 按顺序调用队列中的回调函数
this.handlersQueue.forEach((handler) => {
if (this.state === State.Resolved) {
// 通过 .then(callback, _) 传入的回调函数
handler.handleOnFullfilled(this.result);
} else {
// 通过 .then(_, callback) 或者 .catch(callback) 传入的回调函数
handler.handleOnRejected(this.result);
}
});
// 清空队列,避免重复调用
this.handlersQueue = [];
});
};
//..
}
适配 thenable
这段代码中,resolve 传入了一个新的 promise,你能看出运行结果吗?
new Promise((resolve) => {
const inner = new Promise((resolve) => {
resolve(1); // 把 inner 的 status 设为 resolved, result 设为 1
});
resolve(inner);
}).then((v) => {
console.log("outer", v);
});
console.log("a");
inner
promise resolve 的结果可以被外层的 promise 拿到,为什么呢?
原理是这样的:
resolve
方法的内部实现有一行判断:如果inner
有then
方法(说明 inner 是个 thenable 对象),就调用inner.then(this.resolve, this.reject)
then
是箭头函数
,所以then
里面的this
指向的是外层promise
,而不是 resolve(new Promise(...)) 中的 promise。通过箭头函数的特性,inner 可以由 this 拿到外部 promise 的 handlersQueue,然后让外部 promise 拿到 result
then 回调函数返回值
从 then
的代码实现可以看出,then
内部会创建一个新的 promise
并去 resolve(value)
,这个 value
是我们给到 then
的回调函数的返回值。如果 then
回调没有返回值,那么后面一个 then
回调接收到的参数是 undefined
new Promise((resolve) => {
resolve(1); // 把 promise 内部的状态设为 fulfilled, value 设为 1
})
.then((valueofPrevPromise) => {
// 根据上一个 promise 的 value,生成新的 promise,并 resolve (value),所以 valueofPrevPromise 为 1
console.log("b", valueofPrevPromise);
// then 方法内部会通过这个回调函数生成一个新的 promise, 并且这个回调函数的返回值是新 promise resolve 的值,
// 这里没有返回值,所以下面的 then 接收到的 value 是 undefined
})
.then((valueofPrevPromise) => {
console.log("c", valueofPrevPromise);
});
console.log("a");
// output
//a
//b 1
//c undefined
收尾工作 - catch finally
catch 和 finally 只是语法糖,它们内部仅仅是调用了 then
语法糖之 catch
catch = (onRejected?: (reason: any) => any | MyPromise) => {
return this.then(undefined, onRejected);
};
语法糖之 finally
finally = (onFinally: () => void) => {
return this.then(
(value) => {
onFinally();
return value;
},
(reason) => {
onFinally();
throw reason;
}
);
};
总结
核心原理总结
-
状态如何管理? 通过 resolve/reject 方法扭转状态,并且过滤掉非 pending 状态,实现状态不可逆。
-
Executor 为什么这么写? 使用了 Revealing Constructor Pattern,在构造函数阶段暴露 resolve/reject 方法,使用者只能在构造函数阶段扭转状态。
-
链式调用是怎么实现的? 每个 then 方法都返回一个新的 Promise 对象。
-
错误处理是怎么实现的? Promise 对象可以通过
.catch
或者.then( _ , callback)
方法捕获错误并处理它们。原理是通过 Promise 内部帮我们调用 reject 方法。 -
值穿透是怎么实现的? 原理在 then 的内部实现中,如果我们给 then 传入一个 type 不是 function 的值,then 会构造一个新的 promise,同时把现在的 promise 的 result 设为新的 promise 的 result。
最终完全体
可以在 GitHub 上查看最终源码: MyPromise源码
race、all 等 Promise 静态方法的实现不包括在本篇中,其实理解了本篇的代码,实现它们就很简单了。
References
Learn JavaScript Promises by Building a Custom Implementation
转载自:https://juejin.cn/post/7242493101088555064