欢迎来这里 前端杂谈, 聊聊前端
代码在github
《手写 Promise》是一个经典的问题,基本上大家上手都可以按照自己的理解,写出来一个 promise, 有一天个朋友问我,"手写 Promise 要写到什么程度才是合格的 ?", 这也引起了我的兴趣和思考, "怎么样的 Promise ,才是完美的呢 ? "
完美的 Promise
第一个问题就是怎么样才算是一个完美的 Promise 呢, 其实这个问题也不难,实现一个和原生 Promise "相同"的 Promsie,不就是完美的了, 那么第二个问题也就来了,原生的 Promise 是按照什么标准来实现的呢, 查阅了资料之后知道是按照 [Promises/A+] (https://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
是TypeError
const 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
](https://github.com/promises-a...), 里面目前包含872个测试用例,用于测试 Promise 是否标准
正文开始
首先说明下这边是按照已完成的代码对实现 promise 进行介绍代码在这里, 这里使用的是最终版本,里面注释大致标明了实现的规则编号,其实整体来说经过了很多修改,如果要看整个便携过程,可以commit history, 关注promise_2.js
和promise.js
两个文件
编写的关键点
整体的实现思路主要就是上面的规范了,当然我们也不是说逐条进行实现,而是对规范进行分类,统一去实现:
- promise的状态定义及转变规则和基础运行
- then的实现
- onFulfilled和onRejected的执行及执行时机
- thenable的处理
- promise和then及thenable中对于错误的处理
resolve
和reject
函数的调用次数问题
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 是一个比较有趣的过程,上面如果有什么问题的,欢迎留言多多交流