Promise进阶——如何实现一个Promise库

9次阅读

共计 11184 个字符,预计需要花费 28 分钟才能阅读完成。

概述
从上次更新 Promise/A+ 规范后,已经很久没有更新博客了。之前由于业务需要,完成了一个 TypeScript 语言的 Promise 库。这次我们来和大家一步一步介绍下,我们如何实现一个符合 Promise/A+ 规范的 Promise 库。
如果对 Promise/A+ 规范还不太了解的同学,建议先看看上一篇博客——前端基础知识储备——Promise/A+ 规范。
实现流程
首先,我们来看下,在我实现的这一个 Promise 中,代码由下面这几部分组成:

全局异步函数执行器
常量与属性
类方法
类静态方法

通过上面这四个部分,我们就能够得到一个完整的 Promise。这四个部分互相有关联,接下来我们一个一个模块来看。
全局异步函数执行器
在之前的 Promiz 的源码分析的博客中我有提到过,我们如何来实现一个异步函数执行器。通过 JavaScript 的执行原理我们可以知道,如果要实现异步执行相关函数的话,我们可以选择使用宏任务和微任务,这一点在 Promise/A+ 的规范中也有提及。因此,下面我们提供了一个用宏任务来实现异步函数执行器的代码供大家参考。
let index = 0;

if (global.postMessage) {
global.addEventListener(‘message’, (e) => {
if (e.source === global) {
let id = e.data;
if (isRunningTask) {
nextTick(functionStorage[id]);
} else {
isRunningTask = true;

try {
functionStorage[id]();
} catch (e) {

}
isRunningTask = false;
}

delete functionStorage[id];
functionStorage[id] = void 0;
}
});
}

function nextTick(func) {
if (global.setImmediate) {
global.setImmediate(func);
} else if (global.postMessage) {
functionStorage[++index] = func;
global.postMessage(index, ‘*’)
} else {
setTimeout(func);
}
}
通过上面的代码我们可以看到,我们一共使用了 setImmediate、postMessage、setTimeout 这三个添加宏任务的方法来进行一步函数执行。
常量与属性
说完了如何进行异步函数的执行,我们来看下相关的常量与属性。在实现 Promise 之前,我们需要定义一些常量和类属性,用于后面存储数据。让我们一个一个来看下。
常量
首先,Promise 共有 5 个状态,我们需要用常量来进行定义,具体如下:
enum State {
pending = 0,
resolving = 1,
rejecting = 2,
resolved = 3,
rejected = 4
};
这五个常量分别对应 Promise 中的 5 个状态,相信大家能够从名字中理解,我们就不多讲了。
属性
在 Promise 中,我们需要一些属性来存储数据状态和后续的 Promise 引用,具体如下:
class Promise {
private _value;
private _reason;
private _next = [];
public state: State = 0;
public fn;
public er;
}
我们对属性进行逐一说明:

_value,表示在 resolved 状态时,用来存储当前的值。

_reason,表示在 rejected 状态时,用来存储当前的原因。

_next,表示当前 Promise 后面跟着 then 函数的引用。

fn,表示当前 Promise 中的 then 方法的第一个回调函数。

er,表示当前 Promise 中的 then 方法的的第二个回调函数(即 catch 的第一个参数,下面看 catch 实现方法就能理解)。

类方法
看完了常量与类的属性,我们来看下类的静态方法。
Constructor
首先,如果我们要实现一个 Promise,我们需要一个构造函数来初始化最初的 Promise。具体代码如下:
class Promise {
constructor(resolver?) {
if (typeof resolver !== ‘function’ && resolver !== undefined) {
throw TypeError()
}

if (typeof this !== ‘object’) {
throw TypeError()
}

try {
if (typeof resolver === ‘function’) {
resolver(this.resolve.bind(this), this.reject.bind(this));
}
} catch (e) {
this.reject(e);
}
}
}
从 Promise/A+ 的规范来看,我们可以知道,如果 resolver 存在并且不是一个 function 的话,那么我们就应该抛出一个错误;否则,我们应该将 resolve 和 reject 方法传给 resolver 作为参数。
resolve && reject
那么,resolve 和 reject 方法又是做什么的呢?这两个方法主要是用来让当前的这个 Promise 转换状态的,即从 pending 状态转换为 resolving 或者 rejecting 状态。下面让我们来具体看下代码:
class Promise {
resolve(value) {
if (this.state === State.pending) {
this._value = value;
this.state = State.resolving;

nextTick(this._handleNextTick.bind(this));
}

return this;
}

reject(reason) {
if (this.state === State.pending) {
this._reason = reason;
this.state = State.rejecting;
this._value = void 0;

nextTick(this._handleNextTick.bind(this));
}

return this;
}
}
从上面的代码中我们可以看到,当 resolve 或者 reject 方法被触发时,我们都改变了当前这个 Proimse 的状态,并且异步调用执行了_handleNextTick 方法。状态的改变标志着当前的 Promise 已经从 pending 状态改变成了 resolving 或者 rejecting 状态,而相应_value 和_reson 也表示上一个 Promise 传递给下一个 Promise 的数据。
那么,这个_handleNextTick 方法又是做什么的呢?其实,这个方法的作用很简单,就是用来处理当前这个 Promise 后面跟着的 then 函数传递进来的回调函数 fn 和 er。
then && catch
在了解_handleNextTick 之前,我们们先看下 then 函数和 catch 函数的实现。
class Promise {
public then(fn, er?) {
let promise = new Promise();
promise.fn = fn;
promise.er = er;

if (this.state === State.resolved) {
promise.resolve(this._value);
} else if (this.state === State.rejected) {
promise.reject(this._reason);
} else {
this._next.push(promise);
}

return promise;
}

public catch(er) {
return this.then(null, er);
}
}
因为 catch 函数调用就是一个 then 函数的别名,我们下面就只讨论 then 函数。
在 then 函数执行时,我们会创建一个新的 Promise,然后将传入的两个回调函数用新的 Promise 的属性保存下来。然后,先判断当前的 Promise 的状态,如果已经是 resolved 或者 rejected 状态时,我们立即调用新的 Promise 中 resolve 或者 reject 方法,让下将当前 Promise 的_value 或者_reason 传递给下一个 Promise,并且触发下一个 Promise 的状态改变。如果当前 Promise 的状态仍然为 pending 时,那么就将这个新生成的 Promise 保存下来,等当前这个 Promise 的状态改变后,再触发新的 Promise 变化。最后,我们返回了这个 Promise 的实例。
handleNextTick
看完了 then 函数,我们就可以来看下我们提到过的 handleNextTick 函数。
class Promise {
private _handleNextTick() {
try {
if (this.state === State.resolving && typeof this.fn === ‘function’) {
this._value = this.fn.call(getThis(), this._value);
} else if (this.state === State.rejecting && typeof this.er === ‘function’) {
this._value = this.er.call(getThis(), this._reason);
this.state = 1;
}
} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
this._finishThisTypeScriptPromise();
}

// if promise === x, use TypeError to reject promise
// 如果 promise 和 x 指向同一个对象,那么用 TypeError 作为原因拒绝 promise
if (this._value === this) {
this.state = State.rejecting;
this._reason = new TypeError();
this._value = void 0;
}

this._finishThisTypeScriptPromise();
}
}
我们先来看一个简单版的_handleNextTick 函数,这样能够帮助我们快速理解 Promise 主流程。
异步触发了_handleNextTick 函数后,我们会判断当前用户处于的状态,如果当前 Promise 是 resolving 状态,我们就会调用 fn 函数,即我们在 then 函数调用时给新的 Promise 设置的那个 fn 函数;而如过当前 Promise 是 rejecting 状态,我们就会调用 er 函数。
上面提到的 getThis 方法是用来获取特定的 this 值,具体的规范要求我们将在稍后再进行介绍。
通过执行这两个同步的 fn 或 er 函数,我们能够得到当前 Promise 执行完传入回调后的值。在这里需要说明的是:我们在执行 fn 或者 er 函数之前,我们在_value 和_reason 中存放的值,是上一个 Promise 传递下来的值。只有当执行完了 fn 或者 er 函数后,_value 和_reason 中存放的值才是我们需要传递给下一个 Promise 的值。
大家到这里可能会奇怪,我们的 this 指向没有发生变化,但是为什么我们的 this 指向的是那个新的 Promise,而不是原来的那个 Promise 呢?
我们可以从另外一个角度来看待这个问题:我们当前的这个 Promise 是不是由上一个 Promise 所产生的呢?如果是这种情况的话,我们就可以理解,在上一个 Promise 产生当前 Promise 的时候,就设置了 fn 和 er 两个函数。
大家可能又会问,那么我们第一个 Promise 的 fn 和 er 这两个参数是怎么来的呢?
那么我们就需要仔细看下上面这个逻辑了。下面我们只讨论第一个 Promise 处于 pending 的情况,其余的情况与这种情形基本相同。第一个 Promise 因为没有上一个 Promise 去设置 fn 和 er 两个参数,因此这两个参数的值就是 undefined。所以在上面的逻辑中,我们已经排除了这种情况,直接进入了_finishThisTypeScriptPromise 函数中。
我们在这里需要特别说明下的是,有些人会认为我们在调用 then 函数传入的两个回调函数 fn 和 er 时,当前 Promise 就结束了,其实并不是这样,我们是得到了 fn 或者 er 两个函数的返回值,再将值传递给下一个 Promise 时,上一个 Promise 才会结束。关于这个逻辑我们可以看下_finishThisTypeScriptPromise 函数。
finishThisTypeScriptPromise
_finishThisTypeScriptPromise 函数的代码如下:
class Promise {
private _finishThisTypeScriptPromise() {
if (this.state === State.resolving) {
this.state = State.resolved;

this._next.map((nextTypeScriptPromise) => {
nextTypeScriptPromise.resolve(this._value);
});
} else {
this.state = State.rejected;

this._next.map((nextTypeScriptPromise) => {
nextTypeScriptPromise.reject(this._reason);
});
}
}
}
从_finishThisTypeScriptPromise 函数中我们可以看到,我们在得到了需要传递给下一个 Promise 的_value 或者_reason 后,利用 map 方法逐个调用我们保存的新生成的 Promise 实例,调用它的 resolve 方法,因此我们又触发了这个 Promise 的状态从 pending 转变为 resolving 或者 rejecting。
到这里我们就已经完全了解了一个 Promise 从最开始的创建,到最后结束的整个生命周期。下面我们来看下在 Promise/A+ 规范中提到的一些分支逻辑的处理情况。
上一个 Promise 传递的 value 是 Thenable 实例
首先,让我们来了解下什么是 Thenable 实例。Thenable 实例指的是属性中有 then 函数的对象。Promise 就是的一种特殊的 Thenable 对象。
下面,为了方便讲解,我们将用 Promise 来代替 Thenable 进行讲解,其他的 Thenable 类大家可以参考类似思路进行分析。
如果我们在传递给我们的_value 中是一个 Promise 实例,那么我们必须在等待传入的 Promise 状态转换到 resolved 之后,当前的 Promise 才能够继续往下执行,即我们从传入的 Promise 中得到了一个非 Thenable 返回值时,我们才能用这个值来调用属性中的 fn 或者 er 方法。
那么,我们要怎么样才能获取到传入的这个 Promise 的返回值呢?在 Promise 中其实用到了一个非常巧妙的方法:因为传入的 Promise 中有一个 then 函数(Thenable 定义),因此我们就调用 then 函数,在第一个回调函数 fn 中传入获取_value,触发当前的 Promise 继续执行。如果是触发了第二个回调函数 er,那么就用在 er 中得到的_reason 来拒绝掉当前的 Promise。具体判断逻辑如下:
class Promise {
private _handleNextTick() {
let ref;
let count = 0;

try {
// 判断传入的 this._value 是否为一个 thanable
// check if this._value a thenable
ref = this._value && this._value.then;
} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;

return this._handleNextTick();
}

if (this.state !== State.rejecting && (typeof this._value === ‘object’ || typeof this._value === ‘function’) && typeof ref === ‘function’) {
// add a then function to get the status of the promise
// 在原有 TypeScriptPromise 后增加一个 then 函数用来判断原有 promise 的状态

try {
ref.call(this._value, (value) => {
if (count++) {
return;
}

this._value = value;
this.state = State.resolving;
this._handleNextTick();
}, (reason) => {
if (count++) {
return;
}

this._reason = reason;
this.state = State.rejecting;
this._value = void 0;
this._handleNextTick();
});
} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
this._handleNextTick();
}
} else {
try {
if (this.state === State.resolving && typeof this.fn === ‘function’) {
this._value = this.fn.call(getThis(), this._value);
} else if (this.state === State.rejecting && typeof this.er === ‘function’) {
this._value = this.er.call(getThis(), this._reason);
this.state = 1;
}
} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
this._finishThisTypeScriptPromise();
}

this._finishThisTypeScriptPromise();
}
}
}
promise === value
在 Promise/A+ 规范中,如果返回的_value 值等于用户自身时,需要用 TypeError 错误拒绝掉当前的 Promise。因此我们需要在_handleNextTick 中加入以下判断代码:
class Promise {
private _handleNextTick() {
let ref;
let count = 0;

try {
// 判断传入的 this._value 是否为一个 thanable
// check if this._value a thenable
ref = this._value && this._value.then;
} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;

return this._handleNextTick();
}

if (this.state !== State.rejecting && (typeof this._value === ‘object’ || typeof this._value === ‘function’) && typeof ref === ‘function’) {
// add a then function to get the status of the promise
// 在原有 TypeScriptPromise 后增加一个 then 函数用来判断原有 promise 的状态

} else {
try {
if (this.state === State.resolving && typeof this.fn === ‘function’) {
this._value = this.fn.call(getThis(), this._value);
} else if (this.state === State.rejecting && typeof this.er === ‘function’) {
this._value = this.er.call(getThis(), this._reason);
this.state = 1;
}
} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
this._finishThisTypeScriptPromise();
}

// if promise === x, use TypeError to reject promise
// 如果 promise 和 x 指向同一个对象,那么用 TypeError 作为原因拒绝 promise
if (this._value === this) {
this.state = State.rejecting;
this._reason = new TypeError();
this._value = void 0;
}

this._finishThisTypeScriptPromise();
}
}
}
getThis
在 Promise/A+ 规范中规定:我们在调用 fn 和 er 两个回调函数时,this 的指向有限制。在严格模式下,this 的值应该为 undefined;在宽松模式下时,this 的值应该为 global。
因此,我们还需要提供一个 getThis 函数用于处理上述情况。具体代码如下:
class Promise {

}

function getThis() {
return this;
}
类静态方法
我们通过上面说到的类方法和一些特定分支的逻辑处理,我们就已经实现了一个符合基本功能的 Promise 类。那么,下面我们来看下 ES6 中提供的一些标准 API 我们如何来进行实现。具体 API 如下:

resolve
reject
all
race

下面我们就一个一个方法来看下。
resolve && reject
首先我们来看下最简单的 resolve 和 reject 方法。
class Promise {
public static resolve(value?) {
if (TypeScriptPromise._d !== 1) {
throw TypeError();
}

if (value instanceof TypeScriptPromise) {
return value;
}

return new TypeScriptPromise((resolve) => {
resolve(value);
});
}

public static reject(value?) {
if (TypeScriptPromise._d !== 1) {
throw TypeError();
}

return new TypeScriptPromise((resolve, reject) => {
reject(value);
});
}
}
通过上面代码我们可以看到,resolve 和 reject 方法基本上就是直接使用了内部的 constructor 方法进行 Promise 构建。
all
class Promise {
public static all(arr) {
if (TypeScriptPromise._d !== 1) {
throw TypeError();
}

if (!(arr instanceof Array)) {
return TypeScriptPromise.reject(new TypeError());
}

let promise = new TypeScriptPromise();

function done() {
// 统计还有多少未完成的 TypeScriptPromise
// count the unresolved promise
let unresolvedNumber = arr.filter((element) => {
return element && element.then;
}).length;

if (!unresolvedNumber) {
promise.resolve(arr);
}

arr.map((element, index) => {
if (element && element.then) {
element.then((value) => {
arr[index] = value;
done();
return value;
});
}
});
}

done();

return promise;
}
}
下面我们根据上面的代码来简单说下 all 函数的基本思路。
首先我们需要先创建一个新的 Promise 用于返回,保证后面用户调用 then 函数进行后续逻辑处理时可以设置新 Promise 的 fn 和 er 这两个回调函数。
然后,我们怎么获取上面 Promise 数组中每一个 Promise 的值呢?方法很简单,我们在前面就已经介绍过:我们调用了每一个 Promise 的 then 函数用来获取当前这个 Promise 的值。并且,在每个 Promise 完成时,我们都检查下是否所有的 Promise 都已经完成,如果已经完成,则触发新 Promise 的状态从 pending 转换为 resolving 或者 rejecting。
race
class Promise {
public static race(arr) {
if (TypeScriptPromise._d !== 1) {
throw TypeError();
}

if (!(arr instanceof Array)) {
return TypeScriptPromise.reject(new TypeError());
}

let promise = new TypeScriptPromise();

function done(value?) {
if (value) {
promise.resolve(value);
}

let unresolvedNumber = arr.filter((element) => {
return element && element.then;
}).length;

if (!unresolvedNumber) {
promise.resolve(arr);
}

arr.map((element, index) => {
if (element && element.then) {
element.then((value) => {
arr[index] = value;
done(value);
return value;
});
}
});
}

done();

return promise;
}
}
race 的思路与 all 基本一致。只是我们在处理函数上不同。当我们只要检测到数组中的 Promise 有一个已经转换到了 resolve 或者 rejected 状态(通过没有 then 函数来进行判断)时,就会立即出发新创建的 Promise 示例的状态从 pending 转换为 resolving 或者 rejecting。
总结
我们对 Promise 的异步函数执行器、常量与属性、类方法、类静态方法进行了逐一介绍,让大家对整个 Promise 的构造和声明周期有了一个深度的理解和认知。在整个开发中需要注意到的一些关键点和细节,我在上面也一一说明了。大家只需要按照这个思路,对照 Promise/A+ 规范就能够完成一个符合规范的 Promise 库。
最后,给大家提供一个 Promise/A+ 测试工具,大家实现了自己的 Promise 后,可以使用这个工具来测试是否完全符合整个 Promise/A+ 规范。当然,大家如果想使用我的现成代码,也欢迎大家使用我的代码 Github/typescript-proimse。

正文完
 0