欢迎来这里 前端杂谈, 聊聊前端
代码在github
《手写 Promise》是一个经典的问题,基本上大家上手都可以按照自己的理解,写出来一个 promise, 有一天个朋友问我,"手写 Promise 要写到什么程度才是合格的 ?", 这也引起了我的兴趣和思考, "怎么样的 Promise ,才是完美的呢 ? "
完美的 Promise
第一个问题就是怎么样才算是一个完美的 Promise 呢, 其实这个问题也不难,实现一个和原生 Promise "相同"的 Promsie,不就是完美的了, 那么第二个问题也就来了,原生的 Promise 是按照什么标准来实现的呢, 查阅了资料之后知道是按照 [Promises/A+] (promisesaplus.com/)标准来实现的, 具体的实现在 ECMA - sec-promise-objects 上有记载, 现在标准有了,我们就可以来实现一个"完美的 Promise"了
Promises/A+
接下来我们来看看Promises/A+标准说了啥, 主要是两部分,一个是名词定义,一个是标准描述,其中标准描述由三个部分组成, 接下来我们简单介绍下:
Terminology
这部分是名词定义,主要是描述了各个名词在标准中的定义
promise: 是具有then行为符合规范的方法的object或function, 这里需要注意的是不是function是then,是function中有then方法thenable: 是定义then方法的object或函数,这个和上面promise的区别在于then是一个函数,不一定需要符合规范行为value: 是任何合法的 javascript 值,包括undefined、thenable、promise,这里的value包含了thenable和promise,结合下面的规范,会发现是一个可嵌套的关系exception: 是一个通过throw关键词抛出来的值reason: 表示一个promise状态是rejected的原因
Requirements
这部分是标准的定义,分为以下三个部分
Promise States
一个promise必须是以下三种状态之一
pending- 可以转变成
fulfilled或者rejected状态
- 可以转变成
fulfilled- 需要存在一个
value
- 需要存在一个
rejected- 需要存在一个
reason
- 需要存在一个
当状态是fulfilled 或者 rejected时,状态不可以再变化成其他状态,而value 和reason 也不可以再变化
The then Method
这部分定义了 promise 中 then 方法的行为,then 方法是用来访问promise状态变成fulfilled 或者 rejected 的value 或者reason 的, then 有两个参数,如下:
promise.then(onFulfilled,onRejected)
-
onFulfilled/onRejected- 都是可选参数,如果这两个参数不是函数类型,那么忽略
- 在
promise状态变成fulfilled/rejected之后被调用,会带上value/reason作为函数的参数 - 只会被调用一次
- 需要在
宏任务或者微任务事件循环中完成。 注: 这里对于执行时机的描述比较有趣,可以看看文档 2.2.4 - 两个函数需要被绑定在
global this上运行
-
同一个 Promise可以被多次
then调用,then中的onFulfilled和onRejected必须按照then的调用顺序调用 -
then函数调用之后需要返回一个promise, 这也是promise可以链式调用then的基础promise2 = promise1.then(onFulfilled,onRejected)
- 如果
onFulfilled或者onRejected函数返回了值x, 则运行 Promise Resolution Procedure - 如果
onFulfilled或者onRejected抛出错误e, 则promise2的状态是rejected,并且reason是e - 如果
onFulfilled或者onRejected不是一个函数,而且promise1的状态已经确定fulfilled/rejected, 则promise2
- 如果
The Promise Resolution Procedure
其实大体的标准部分在Promise States 和 The then Method已经描述完了,这部分主要规定了一个抽象的操作promise resolution procedure, 用来描述当then 的 onFulfilled或者onRejected 返回值x时,需要怎么样去进行操作,把表达式记为[[Resolve]](promise,x), 这部分也是整个 Promise 实现最复杂的部分,我们一起看看他规定了什么
[[Resolve]](promise,x)
-
当
promise和x是同一个对象时,promise为rejected,reason是TypeErrorconst promise = Promise.resolve().then(()=>promise); // TypeError -
如果
x是一个Promise时,则promise的状态要与x同步 -
如果
x是一个object或者一个function, 这部分是最复杂的-
首先要把
x.then存储在一个中间变量then, 为什么要这么做可以看文档 3.5,然后根据不同条件进行处理 -
如果获取
x.then的时候就抛出错误e,则promise状态变成rejected,reason是e -
如果
then是一个函数,那么这就是我们定义里面的thenable, 这时候绑定x为 this并调用then,传入promise的resolvePromise和rejectPromise作为两个参数then.call(x, resolvePromise, rejectPromise)
接下来判断调用的结果
-
如果
resolvePromise被调用,value是y, 则调用[[Resolve]](promise,y) -
如果
rejectPromise被调用,reason是e, 则promise状态变成rejected,reason是e -
如果
resolvePromise和rejectPromise都被调用,则以第一个调用会准,后续的调用都被忽略 -
如果调用过程中抛出了错误
e- 如果抛出之前
resolvePromise或者rejectPromise已经被调用了,那么就忽略错误 - 后者的话,则
promise状态变成rejected,reason是e
- 如果抛出之前
-
-
如果
then不是一个函数,那么promise状态变成fulfilled,value是x
-
-
如果
x不是一个object或者function, 则promise状态变成fulfilled,value是x
这里面最复杂的就是在 resolvePromise 被调用,value是y 这部分,实现的是thenable 的递归函数
上面就是如何实现一个"完美"的 Promise 的规范了,总的来说比较复杂的是在The Promise Resolution Procedure 和对于错误和调用边界的情况,下面我们将开始动手,实现一个"完美"的Promise
如何测试你的 Promise
前面介绍了 Promise/A+规范, 那么如何测试你的实现是完全实现了规范的呢, 这里Promise/A+ 提供了 promises-tests
, 里面目前包含872个测试用例,用于测试 Promise 是否标准
正文开始
首先说明下这边是按照已完成的代码对实现 promise 进行介绍代码在这里, 这里使用的是最终版本,里面注释大致标明了实现的规则编号,其实整体来说经过了很多修改,如果要看整个便携过程,可以commit history, 关注promise_2.js 和promise.js 两个文件
编写的关键点
整体的实现思路主要就是上面的规范了,当然我们也不是说逐条进行实现,而是对规范进行分类,统一去实现:
promise的状态定义及转变规则和基础运行
const Promise_State = {
PENDING: "pending",
FULFILLED: "fulfilled",
REJECTED: "rejected",
};
class MyPromise {
constructor(executerFn) {
this.state = Promise_State.PENDING;
this.thenSet = [];
try {
executerFn(this._resolveFn.bind(this), this._rejectedFn.bind(this));
} catch (e) {
this._rejectedFn.call(this, e);
}
}
}
在构造函数中初始化状态为pending,并且运行传入构造函数的executerFn,传入resovlePromise、rejectePromise两个参数
然后我们接下去就要实现 resolvePromise,rejectPromise 这两个函数
_resolveFn(result) {
// 2.1.2
if (this._checkStateCanChange()) {
this.state = Promise_State.FULFILLED;
this.result = result;
this._tryRunThen();
}
}
_rejectedFn(rejectedReason) {
//2.1.3
if (this._checkStateCanChange()) {
this.state = Promise_State.REJECTED;
this.rejectedReason = rejectedReason;
this._tryRunThen();
}
}
_checkStateCanChange() {
//2.1.1
return this.state === Promise_State.PENDING;
}
这里主要是通过_checkStateCanChange 判断是否可执行的状态,然后进行状态变更,value、reason的赋值,然后尝试运行then方法注册的函数
这时候我们的promise 已经可以这么调用了
const p = new MyPromise((resolve,reject)=>{
resolve('do resolve');
// reject('do reject');
});
then的实现
接下来我们实现then 函数,首先有个简单的问题: 『then方法是什么时候执行的?』,有人会回答,是在 promise 状态变成resolve或者rejected 的之后执行的,这个乍一看好像没毛病,但是其实是有毛病的,正确的说法应该是
『then方法是立即执行的,then方法传入的
onFulfilled、onRejected参数会在 promise 状态变成resolve或者rejected后执行
我们先上代码
then(onFulfilled, onRejected) {
const nextThen = [];
const nextPromise = new MyPromise((resolve, reject) => {
nextThen[1] = resolve;
nextThen[2] = reject;
});
nextThen[0] = nextPromise;
//2.2.6
this.thenSet.push([onFulfilled, onRejected, nextThen]);
this._runMicroTask(() => this._tryRunThen());
return nextThen[0];
}
代码看起来也挺简单的,主要逻辑就是构造一个新的 promise,然后把 onFulfilled、onRejected还有新构造的 promise 的resolve、reject 存储到thenSet集合中,然后返回这个新构建的promise, 这时候我们的代码已经可以这样子调用
const p = new MyPromise((resolve,reject)=>{
resolve('do resolve');
// reject('do reject');
});
p.then((value)=>{
console.log(`resolve p1 ${value}`);
},(reason)=>{
console.log(`reject p1 ${reason}`);
}).then((value)=>console.log(`resolve pp1 ${value}`));
p.then((value)=>{
console.log(`resolve p2 ${value}`);
},(reason)=>{
console.log(`reject p2 ${reason}`);
});
onFulfilled和onRejected的执行及执行时机
onFulFilled 和onRejected 会在 promise 状态变成fulfilled或者rejected之后被调用,结合then方法被调用的时机,判断时候状态可以调用需要在两个地方做
-
在
resolvePromise、resolvePromise被调用的时候(判断是否有调用了then注册了onFulfilled和onRejected) -
在
then函数被调用的时候(判断是否 promise状态已经变成了fulfilled或rejected)
这两个时机会调用以下函数
_tryRunThen() {
if (this.state !== Promise_State.PENDING) {
//2.2.6
while (this.thenSet.length) {
const thenFn = this.thenSet.shift();
if (this.state === Promise_State.FULFILLED) {
this._runThenFulfilled(thenFn);
} else if (this.state === Promise_State.REJECTED) {
this._runThenRejected(thenFn);
}
}
}
}
这里会判断时候需要调用then注册的函数,然后根据 promise 的状态将 thenSet 中的函数进行对应的调用
_runThenFulfilled(thenFn) {
const onFulfilledFn = thenFn[0];
const [resolve, reject] = this._runBothOneTimeFunction(
thenFn[2][1],
thenFn[2][2]
);
if (!onFulfilledFn || typeOf(onFulfilledFn) !== "Function") {
// 2.2.73
resolve(this.result);
} else {
this._runThenWrap(
onFulfilledFn,
this.result,
thenFn[2][0],
resolve,
reject
);
}
}
_runThenFulfilled和_runThenRejected 相似,这里就通过一个进行讲解,
首先我们判断onFulfilled或者onRejected 的合法性
- 如果不合法则不执行,直接将promise 的
value或reason透传给之前返回给then的那个 promise,这个时候相当于then的 promise 的状态和原来的 promise 的状态相同 - 如果合法,则执行
onFulfilled或者onRejected
_runThenWrap(onFn, fnVal, prevPromise, resolve, reject) {
this._runMicroTask(() => {
try {
const thenResult = onFn(fnVal);
if (thenResult instanceof MyPromise) {
if (prevPromise === thenResult) {
//2.3.1
reject(new TypeError());
} else {
//2.3.2
thenResult.then(resolve, reject);
}
} else {
// ... thenable handler code
// 2.3.3.4
// 2.3.4
resolve(thenResult);
}
} catch (e) {
reject(e);
}
});
}
这里先截取一小段_runThenWrap,主要是说明onFulfilled或onRejected的运行,这部分在规范中有这样子的一个描述
onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
简单来说就是onFulfilled 和onRejected要在执行上下文里面没有除了platform code 之后才能执行,这段听起来有点拗口,其实说人话就是我们经常说的要在微任务、宏任务
所以我们这里包装了_runMicroTask方法,用于封装这部分执行的逻辑
_runMicroTask(fn) {
// 2.2.4
queueMicrotask(fn);
}
这里使用queueMicrotask作为微任务的实现, 当然这个有兼容性问题,具体可以看caniuse
实现的方法还有很多,比如setTimeout、setImmediate、 MutationObserver、process.nextTick
然后将value或reason作为参数执行onFulfilled或onRejected,然后获取返回值thenResult,接下来就会有几个判断的分支
- 如果
thenResult是一个 promise-
判断是否和
then返回的 promise 是相同的,如果是抛出TypeError -
传递
then返回的 promise 的resolve和reject,作为thenResult.then的onFulFilled和onRejected函数
-
- 如果
thenResult不是一个 promise- 判断是否是
thenable,这部分我们在下面进行讲解 - 如果以上判断都不是,那么将
thenResult作为参数,调用resolvePromise
- 判断是否是
thenable的处理
thenable应该说是实现里面最复杂的一个部分了,首先,我们要根据定义判断上部分的thenResult是否是thenable
if (
typeOf(thenResult) === "Object" ||
typeOf(thenResult) === "Function"
) {
//2.3.3.1
const thenFunction = thenResult.then;
if (typeOf(thenFunction) === "Function") {
// is thenable
}
}
可以看到 需要判断是否是一个Object或者Function,然后再判断thenResult.then 是不是个 Function,那么有人会问,能不能写成这样子
if (
(typeOf(thenResult) === "Object" ||
typeOf(thenResult) === "Function") && (typeOf(thenResult.then) === 'Function')
) {
// is thenable
}
刚开始我也是这么写的,然后发现测试用例跑不过,最后去看了规范,有这么一段3.5
简单来说就是为了保证测试和调用的一致性,先把thenResult.then进行存储再判断和运行是有必要的,多次访问属性可能会返回不同的值
接下去就是thenable的处理逻辑了
简单来说thenable 的处理逻辑有两种情况
- 在 promise 的
then或者resolve中处理thenable的情况 - 在
thenable的then回调中处理value还是thenable的情况
这里用在 promise 的then的thenable调用进行讲述:
_thenableResolve(result, resolve, reject) {
try {
if (result instanceof MyPromise) {
// 2.3.2
result.then(resolve, reject);
return true;
}
if (typeOf(result) === "Object" || typeOf(result) === "Function") {
const thenFn = result.then;
if (typeOf(thenFn) === "Function") {
// 2.3.3.3
thenFn(resolve, reject);
return true;
}
}
} catch (e) {
//2.3.3.3.4
reject(e);
return true;
}
}
const [resolvePromise, rejectPromise] =
this._runBothOneTimeFunction(
(result) => {
if (!this._thenableResolve(result, resolve, reject)) {
resolve(result);
}
},
(errorReason) => {
reject(errorReason);
}
);
try {
thenFunction.call(thenResult, resolvePromise, rejectPromise);
} catch (e) {
//2.3.3.2
rejectPromise(e);
}
这里我们构造了resolvePromise和rejectPromise,然后调用 thenFunction, 在函数逻辑中处理完成之后将会调用resolvePromise或者rejectPromise, 这时候如果result是一个 thenable,那么就会继续传递下去,直到不是thenable,调用resolve或者reject
我们要注意的是 promise 的then方法和thenable 的then方法是有不同的地方的
- promise 的
then有两个参数,一个是fulfilled,一个是rejected,在前面的 promise状态改变之后会回调对应的函数 thenable的then也有两个参数,这两个参数是提供给thenable调用完成进行回调的resolve和reject方法,如果thenable的回调值还是一个thenable,那么会按照这个逻辑调用下去,直到是一个非thenable,就会调用离thenable往上回溯最近的一个 promies 的resolve或者reject
到这里,我们的promise 已经可以支持thenable的运行
new MyPromise((resolve)=>{
resolve({
then:(onFulfilled,onRejected)=>{
console.log('do something');
onFulfilled('hello');
}
})
}).then((result)=>{
return {
then:(onFulfilled,onRejected)=>{
onRejected('world');
}
}
});
promise和then及thenable中对于错误的处理
错误处理指的是在运行过程中出现的错误要进行捕获处理,基本上使用 try/catch 在捕获到错误之后调用 reject 回调,这部分比较简单,可以直接看代码
resolve和reject函数的调用次数问题
一个 promise 中的resolve和reject调用可以说是互斥而且唯一的,就是这两个函数只能有一个被调用,而且调用一次,这个说起来比较简单,但是和错误场景在一起的时候,就会有一定的复杂性
本来可能是这样子的
if(something true){
resolve();
}else {
reject();
}
加上错误场景之后
try{
if(something true){
resolve();
throw "some error";
}else {
reject();
}
}catch(e){
reject(e);
}
这时候判断就会无效了, 因此我们按照通过一个工具类来包装出两个互斥的函数,来达到目的
_runBothOneTimeFunction(resolveFn, rejectFn) {
let isRun = false;
function getMutuallyExclusiveFn(fn) {
return function (val) {
if (!isRun) {
isRun = true;
fn(val);
}
};
}
return [
getMutuallyExclusiveFn(resolveFn),
getMutuallyExclusiveFn(rejectFn),
];
}
至此,我们一个完全符合Promise/A+ 标准的 Promise,就完成了, 完整代码在这里
等等,是不是少了些什么
有人看到这里会说,这就完了吗?
我经常使用的catch、finally,还有静态方法Promise.resolve、Promise.reject、Promise.all/race/any/allSettled方法呢?
其实从标准来说,Promise/A+的标准就是前面讲述的部分,只定义了then方法,而我们日常使用的其他方法,其实也都是在then 方法上面去派生的,比如catch 方法
MyPromise.prototype.catch = function (catchFn) {
return this.then(null, catchFn);
};
具体的方法其实也实现了,具体可以看promise_api
最后
最后是想分享下这次这个 promise 编写的过程,从上面的讲述看似很顺利,但是其实在编写的时候,我基本上是简单了过了以下标准,然后按照自己的理解,结合promises-tests单元测试用例来编写的,这种开发模式其实就是TDD(测试驱动开发 (Test-driven development)),这种开发模式会大大减轻发人员编程时候对于边界场景没有覆盖的心智负担,但是反过来,对于测试用例的便携质量要求就很高了
总体来说这次便携 promise 是一个比较有趣的过程,上面如果有什么问题的,欢迎留言多多交流