什么是 Promise
相信大家对 Promise 已经都很熟悉了,以下是 MDN 上面的介绍:
Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。
我们常用的方法有实例方法then/catch/finally,还有类方法all/race等。出于篇幅的考虑,本文只会介绍then/catch/finally。
设计
编码之前,我们可以想一下要实现的功能有什么,或者说,Promise 的特点是什么。
- 状态有
pending、fulfilled、rejected - 状态只能由
pending转换成fulfilled或rejected,转换之后状态已不可再转换。 - 链式调用
这里只简单描述了 3 个特点,其实还有很多没列出来的,完整的内容可以查阅规范。规范描述了 Promise 应如何实现,我们所熟知的 Promise 性质也在规范中声明。
Promise/A+规范是社区中最简洁的规范之一,有兴趣的可以查阅一下。
本文中 Promise 的实现会参考Promise/A+规范,但不会完全按照规范实现,主要是因为懒。
编码
构造函数
构造函数内要做的有参数判断及初始化 Promise 实例状态,并且执行传入的函数参数。
_onResolves/_onRejects分别用于保存 Promise resolved/rejected 之后要执行的回调队列。
_state描述 Promise 当前的状态,_value是使得 Promise 状态变化的值或原因,这个值会传入到回调中。
在resolve/reject函数内,只有当前实例的状态不是 pending 才会去修改状态。
function Promise(fn) {
if (typeof this !== 'object') throw Error('必须通过new调用Promise');
if (typeof fn !== 'function') throw Error('传入Promise的参数必须是函数');
let that = this;
this._onResolves = [];
this._onRejects = [];
this._state = 0; // 0-pending 1-resolve 2 reject;
this._value = null; // 存放value/reason
fn(
function resolve(value) {
if (that._state) return;
that._state = 1;
that._value = value;
transition(that._onResolves, value);
},
function reject(reason) {
if (that._state) return;
that._state = 2;
that._value = reason;
transition(that._onRejects, reason);
}
);
}
transition是遍历队列执行回调的辅助函数,对于当前实例注册,所有传入_onResolves回调的参数值、所有传入_onRejects回调的参数值都是一样。
function transition(fns, val) {
for (let i = 0, len = fns.length; i < len; i++) {
fns[i](val);
}
fns.length = 0;
}
需要注意的是,在构造函数内部定义的实例属性,实际上可以通过返回的实例直接修改的。为了偷懒,在这里假设不会被修改。
各位看官不妨可以想想,如何去避免这些属性被开发者访问和修改。
then
编码之前可以想一下 then 函数内要做的操作。
- 需要返回一个新的 Promise 实例以支持链式调用,这里记为
promise2 - 当 Promise 实例状态已经改变,直接执行回调函数即可
- 状态未改变时需要将回调添加到队列中
- 不管回调的是
onResolve还是onReject,返回值都是promise2的_value,只会影响到promise2的状态,并且 - 在执行回调的过程中抛出错误,则
promise2必须是 reject 的,并且 resaon 是抛出的错误。
这些在规范中2.2 The then Method都已列出,这里就不多赘述了。
接着来看具体实现。首先要先判断传入参数的类型,如果不是函数则不执行,并且需要创建并返回一个 Promise 以供链式调用。
将promise2的resolve/reject缓存起来,后面回调入队时会用到。
Promise.prototype.then = function(onRes, onRej) {
const { _state, _onResolves, _onRejects, _value } = this;
const onResIsFunction = typeof onRes === 'function';
const onRejIsFunction = typeof onRej === 'function';
let _resolveNext, _rejectNext;
const promise2 = new Promise((resolve, reject) => {
_resolveNext = resolve;
_rejectNext = reject;
});
// 省略了switch case语句,不同的状态回调入队列的操作也不同
return promise2;
};
实现之前还是要先看一下规范 2.2.7,这一小节描述了调用 then 的 Promise 实例与返回 Promise 实例的关系。
这里有promise2 = promise1.then(onFulfilled, onRejected);。
首先,不管执行了onFulfilled或onRejected,都将回调的返回值x作为参数传入 promise2 的 resolve 函数。
[[Resolve]](promise2, x)是一个抽象操作,用于令promise2的状态根据x来进行转变。只考虑返回值是同步对象,这里我们可以简单看成是 resolve(x) 即可。
如果在执行回调的过程中抛出错误e,需要将e作为参数传入 promise2 的 reject 函数。
第三、第四点描述了链式调用过程中的处理。
如果 promise1 是 resolved 且onFulfilled不是函数,则用 promise1 的 value 传入 promise2 的 resolve 函数。
例如,promise1 是 reovled 状态,由于后面的几个 promise 都没添加onResolve回调,所以会被忽略,最后的onSuccess会执行,传入的参数是 promise1 的 value。
promise1
.catch(fn1)
.catch(fn2)
.catch(fn3);
.then(onSuccess)
如果 promise2 是 rejected 且onRejected不是函数,则用 promise1 的 reason 传入 promise2 的 reject 函数。意思就是如果在链式调用过程报错,则这个错误由最接近错误的 promise 处理。例如,在fn1抛出错误时,由于后面的几个 promise 都没添加onReject回调,所以错误应由最后 catch 添加的onError处理
promise1
.then(fn1)
.then(fn2)
.then(fn3)
.catch(onError);
这里入队的是封装了实际回调操作的函数。 执行回调在 trycatch 块中执行,以符合规范 2。
对于状态为 pending 的情况,当状态更新时,除了执行回调,还要更新 promise2 的状态。如果存在回调,直接将回调的返回值传入_resolveNext,如果没有回调,直接将_resolveNext/_rejectNext入队即可,value/reason与 promise1 的value/reason保持一致。
switch (_state) {
case 0:
let onResWrapper = onResIsFunction
? function(result) {
try {
_resolveNext(onRes(result)); // 规范1
} catch (error) {
_rejectNext(error); // 规范2
}
}
: _resolveNext; // 规范3
let onRejWrapper = onRejIsFunction
? function(result) {
try {
_resolveNext(onRej(result));
} catch (error) {
_rejectNext(error);
}
}
: _rejectNext; // 规范4
_onResolves.push(onResWrapper);
_onRejects.push(onRejWrapper);
break;
// ...
default:
break;
}
对于状态为 resolved 或 rejected 的对象,调用的时候直接指向即可。
switch (_state) {
case 0:
// ...
case 1:
transition([
() => {
try {
onResIsFunction ? _resolveNext(onRes(_value)) : _resolveNext(_value); // 规范1 3
} catch (error) {
_rejectNext(error); // 规范2
}
}
]);
break;
case 2:
transition([
() => {
try {
onRejIsFunction ? _resolveNext(onRej(_value)) : _rejectNext(_value); // 规范1 4
} catch (error) {
_rejectNext(error); // 规范2
}
}
]);
break;
}
catch 与 finally
实现的重点和难点都在then,catch/finally可以当成是对then的封装。
Promise.prototype.catch = function(onRej) {
return this.then(undefined, onRej);
};
Promise.prototype.finally = function(fn) {
return this.then(fn, fn);
};
测试
设计了几个用于测试破产版 Promise 的破产版测试用例。
let p1 = new Promise(resolve => {
resolve('p1 resolved');
});
p1.catch(() => {
console.log('expected not to log');
})
.catch(() => {
console.log('expected not to log');
})
.then(val => {
console.log('expected to log after 2 catch', val);
});
let p2 = p1.then(
val => {
console.log('expected p1 resolved', val);
return 'This is a promise returned by p1.then';
},
reason => {
console.log('p1 rejected', reason);
}
);
p1.finally(() => {
console.log('p1 finally');
});
p2.then(val => {
console.log(val);
});
p2.then(val => {
throw Error('throw in then');
})
.then(
() => {
console.log('expected not to log');
},
e => {
console.log(e.message);
}
)
.then(() => {
console.log('expect to log after catch');
});
p2.then(val => {
throw Error('throw in chaining then');
})
.then(() => {
console.log('expected not to log');
})
.catch(e => {
console.log(e.message);
});
let rejectdPromise = new Promise((resolve, reject) => {
reject(2);
});
rejectdPromise
.catch(reason => {
console.log('p rejected', reason);
return 'This is a promise returned by catch';
})
.then(val => {
console.log(val);
});
将上面用例复制到 Chrome console 中执行一下,得到:

将 Promise 实现的代码和用例复制到 console 中再执行一下,得到:

咦,输出是正确的,但顺序是不正确的,问题出在回调的执行时机,Promise 是执行完同步代码后再执行回调,并且回调是微任务。
需要稍微改动一下transition,为了偷懒这里直接在将 for 循环放在setTimeout的回调里执行。如果想按照微任务实现,可以参考Vue.nextTick的源代码,使用了MutationObserver。
function transition(fns, val) {
setTimeout(() => {
for (let i = 0, len = fns.length; i < len; i++) {
fns[i](val);
}
fns.length = 0;
});
}
修改后的输出如下:

大功告成。
总结
以上就是一个简单的 Promise 实现(完整的代码戳我),但其实还有很多规范中的功能没有考虑的,并不能覆盖所有的用例。比如,在 Promise 里的构造函数中 resolve 另一个 Promise,或者在 then 里面返回一个另一个 Promise,这种情况下,新生成的 Promise 的状态应该和传入 resolve 的 Promise 状态保持一致。
如果你想自己实现一个 Promise 库并开源,还是需要完整地根据规范来设计,并且要有一定的测试用例。Promise/A+组织开源了一个测试用例库promises-tests,这个库包含了 700+个测试用例,用于测试你的 Promise 库是否符合 A+ 规范。
有什么问题或疑问,欢迎交流。