自己动手实现一个 Promise

Promise 是什么?Promise 是一种异步编程的解决方案,代替了以前在异步函数中传入回调函数的写法,解决了深层嵌套引起的回调地狱问题。关于 Promise 的实现也有很多种,如 ES6 Promise, Bluebird, Q 等,但它们都遵循了一种规范,那就是 Promise/A+ 规范。那么今天我们就按照这个规范自己也实现一个 Promise。

Promise 的基本用法

首先来通过一段代码看一下 Promise 的基本用法:

console.log(1);

const promise = new Promise((resolve, reject) => {
console.log(2);
setTimeout(() => {
resolve(3);
}, 0);
});

promise.then((value) => {
console.log('success: ', value);
}, (reason) => {
console.log('fail: ', reason);
});

console.log(4);

可以先猜一下这段代码的输出结果,后面再给出答案☺️,我们把代码分解一下,可以看出有以下特点:

  • 实例化一个 Promise 对象
  • 进行 resolve 或者 reject 处理
  • 调用 then 方法

下面来分别看一下各个阶段所进行的处理。

实例化一个 Promise 对象

new 一个 Promise 对象,传入一个函数作为参数,我们可以称之为 executor,那么 executor 就是:

(resolve, reject) => {
console.log(2);
setTimeout(() => {
resolve(3);
}, 0);
}

resolve 或者 reject

首先来看一下 Promise 的三种状态:

  • pending: 等待状态
  • fulfilled: 成功状态
  • rejected: 失败状态

它们的状态只可以从 pending 转化为 fulfilled,或者从 pending 转化为 rejected,除此之外不能进行任何状态转化。

Promise 被实例化后首先是 pending 状态,然后会在 executor 内部进行处理,如果成功的话,就调用 resolve(value) 方法,传入成功的结果 value,这时状态会转化为 fulfilled; 如果失败的话,就调用 reject(reason) 方法,传入失败的原因 reason,这时状态会转化为 rejected

then 方法

对于一个 Promise,无论是成功(resolve)还是失败(reject),都可以调用 then() 方法,then 方法会接收两个参数,一个成功的回调,一个失败的回调,如果一个 Promise 被 resolve 了,那么就调用成功的回调,否则(被 reject )就调用失败的回调。

再来回头看开始的例子:

// 用法:then(onFulfilled, onRejected)
promise.then((value) => {
console.log('success: ', value);
}, (reason) => {
console.log('fail: ', reason);
});

由于 promise 是被 resolve 了,因此会执行成功的回调,输出:success: 3

在一个 Promise 内部,executor 会立即执行,then() 方法里的回调会在被 resolvereject 之后执行,并且回调是异步的,因此一开始给出例子的输出结果顺序为:

1  2  4  success: 3

实现 Promise 构造函数

根据前面 Promise 的用法,我们首先来看一下实现一个 Promise 所需要的基础结构:

class Promise {
constructor(executor) {
const resolve = () => {
// TODO
};

const reject = () => {
// TODO
};

executor(resolve, reject);
}
}

// 导出模块
module.exports = Promise;

// 对于 es6 语法的兼容问题,可以使用 rollup 或 webpack 打包,本文不再多讲

这里我们创建了一个 Promise 类,然后通过构造函数让 executor 执行,同时传入 resolve 和 reject 参数,这样就满足了 Promise 实例化时所需要的基础语法:

new Promise((resolve, reject) => {
resolve(123);
// reject(456);
});

那么然后该怎么做呢?根据 Promise/A+ 规范:一个 Promise 会有三种状态:pending, fulfilled, rejected;resolve 后状态会从 pending 转化为 fulfilled,并且会传入成功的结果;reject 后状态会从 pending 转化为 rejected, 并且要传入失败的原因。

通过规范我们再来完善下构造函数:

class Promise {
constructor(executor) {
// 初始状态 pending
this.status = 'pending';
this.value = undefined;
this.reason = undefined;

const resolve = (value) => {
// 状态从 pending 转化为 fulfilled
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = value;
}
}

const reject = (reason) => {
// 状态从 pending 转化为 rejected
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = reason;
}
}

try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
}

现在构造函数已经实现了一部分了,但实际上还没有什么用,因为还没跟 then() 方法关联起来,并且也无法执行成功的回调或者失败的回调。接下来我们看一下 then() 方法的实现。

实现 then 方法

一般调用 then() 方法,是通过实例化后的方法调用的,因此 then 肯定是原型上的一个方法,并且会接收 onFulfilledonRejected 两个参数,当 resolve 之后执行 onFulfilled,当 reject 后会执行 onRejected。

这是一个使用示例:

promise.then((value) => {
console.log('success: ', value);
}, (reason) => {
console.log('fail: ', reason);
});

then 方法分析

那么在 then 方法里 onFulfilled 和 onRejected 该什么时候执行呢?这个就应该根据 Promise 的状态判断了, 分为以下三种情况:

  • 状态为 fulfilled: 执行 onFulfilled 回调
  • 状态为 rejected: 执行 onRejected 回调
  • 状态为 pending: 此时需要把 onFulfilled 回调和 onRejected 回调保存起来,等到 Promise 被 resolve 或 reject 时再执行

并且,根据 Promise/A+ 规范,onFulfilled 和 onRejected 必须要异步执行,在这里我们通过 setTimeout 来进行处理,根据这些信息,我们来编写一下 then 方法。

编写 then 方法

class Promise {
// ... 代码有省略
// ...
then(onFulfilled, onRejected) {
// 对于回调不是函数的处理
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };

if (this.status === 'fulfilled') {
setTimeout(() => {
onFulfilled(this.value);
}, 0);
}

if (this.status === 'rejected') {
setTimeout(() => {
onRejected(this.reason);
}, 0);
}

if (this.status === 'pending') {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
onFulfilled(this.value);
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
onRejected(this.reason);
}, 0);
});
}
}
}

在 Promise 为 pending 状态时,我们把 onFulfilled 和 onRejected 存在了数组里,等到 resolve 或者 reject 时执行,并且我们在每次 onFulfilled 或者 onRejected 外层都加了一层 setTimeout,以确保它们能够异步执行。

当 resolve 或者 reject 后执行回调

那么我们还需要在 constructor 里做如下处理,在合适的时机执行回调:

// ... 代码有省略
const resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = value;
// 执行成功的回调
this.onFulfilledCallbacks.forEach(fn => fn());
}
}

const reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = reason;
// 执行失败的回调
this.onRejectedCallbacks.forEach(fn => fn());
}
}
// ... 代码有省略

好了,到目前为止,then 方法已经初步实现了,但是却不能链式调用,在成功的回调或者失败的回调里也是只能对值类型进行处理,如果 resolve 的是一个 Promise,那么就无法处理了,下面就来进一步加强 then 方法的实现。

then 方法的链式调用

提到链式调用,我们很可能就会想到 this,直接让 then 方法返回 this 不就可以了嘛?答案是不行,因为 Promise 的状态一旦从 pending 变为 fulfilled 或者 rejected 之后就不能再变了,为了防止再次变化不能使用 this,同时 Promise/A+ 规范 里也有提到,then 方法需要返回一个新的 Promise,可以称之为 promise2。

另外,onFulfilled 或者 onRejected 会返回一个值 x(x 可能是普通值,也可能是一个 Promise),x 会被递归处理,如果是普通值则直接 resolve,如果是 Promise 则会调用 then 方法进行处理。取值的过程中如果出错,需要进行 reject 处理。

Ok,下面就来处理一下这个逻辑:

class Promise {
// ...
// ... 代码有省略
// ...
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };

const promise2 = new Promise((resolve, reject) => {
if (this.status === 'fulfilled') {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}

if (this.status === 'rejected') {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
};
}, 0);
}

if (this.status === 'pending') {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
};
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
};
}, 0);
});
}
});

// 返回一个新的 Promise
return promise2;
}
}

function resolvePromise(promise2, x, resolve, reject) {
// 待完善...
}

目前我们在 then 方法里新创建了一个 promise2,并且会做 return 处理,对于 x,我们新创建一个 resolvePromise 方法进行处理,根据 Promise/A+ 规范 的对应描述,我们来编写如下代码:

function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('不能循环引用'));
}

// 可能是一个 promise 或者 thenable 对象
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
let called = false;
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, (value) => {
if (called) return;
called = true;
// resolve(x)
// 递归调用,这里的 value 可能也是个 Promise
resolvePromise(x, value, resolve, reject);
}, (reason) => {
if (called) return;
called = true;
reject(reason);
});
} else {
// x 为 {} 的情况
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}

} else {
// 普通值直接 resolve
resolve(x);
}
}

此时此刻我们的 then() 方法已经编写完毕,对于 Promise/A+ 规范而言,我们已经全部实现了,下面我们就来测试一下是否符合规范。

测试是否符合规范

首先要安装一个测试用的 npm 包:

npm i promises-aplus-tests -g

然后再我们编写 Promise 的文件里面添加如下代码,以供测试:

// ...无关代码已省略
Promise.deferred = Promise.defer = function () {
var dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}

假设我们的文件名为 promise.js,运行如下代码:promises-aplus-tests promise.js,如果发现没有错误提示,那么就说明我们编写的代码已经顺利通过测试了!

为了完善我们的 Promise,下面还需要继续实现以下这些方法:catch(), Promise.finally(), Promise.resolve(), Promise.reject(), Promise.all(), Promise.race()。其实它们全部跟 then() 方法有关,then 我们已经实现了,其它的实现起来就会很简单了。

实现 catch 方法

catch() 方法其实就是一个特殊的 then 方法,一般会放在 Promise 链的最后边,防止前面出现错误而没有捕获。

class Promise {
// ...代码有省略
catch(onRejected) {
return this.then(null, onRejected);
}
}

// 模块导出
module.exports = Promise;

实现 finally 方法

finally() 方法是一个原型上的方法,对于一个 Promise 而言,无论成功还是失败 finally 都会执行,同时还不会影响当前的调用链,因此我们可以这么做:

class Promise {
// ...代码有省略
finally(fn) {
return this.then((value) => {
fn();
return value;
}, (reason) => {
fn();
return reason;
});
}
}

实现 Promise.resolve 方法

这个方法可以把现有的对象(也可能是值)转化为 Promise 对象,因此我们直接在 Promise 里面 resolve 即可,由于是一个静态方法,我们可以直接挂在 Promise 类上。

Promise.resolve = (value) => {
return new Promise((resolve, reject) => resolve(value));
};

实现 Promise.reject 方法

这个方法跟也会返回一个 Promise 对象,但是状态为 rejected,相当于在 Promise 里面进行了 reject 处理。

Promise.reject = (reason) => {
return new Promise((resolve, reject) => reject(reason));
};

实现 all 方法

Promise.all() 接收一个数组作为参数,返回一个 Promise,一般用于处理多个 Promise 的并发情况,直到最慢的 Promise 完成后,再把所有 Promise 的结果作为一个数组进行 resolve 处理;如果其中有一个被 reject 了,那么整体就会被 reject。

Promise.all = (promises) => {
return new Promise((resolve, reject) => {
let result = [];
let counter = 0;

function handleResult(value, i) {
result[i] = value;
// 全部完成后再统一 resolve
if (++counter === result.length) {
resolve(result);
}
}

promises.forEach((item, i) => {
if (typeof item.then === 'function') {
item.then((value) => {
handleResult(value, i);
}, (reason) => {
// 只要有一个失败,那么就直接 reject
reject(reason);
});
} else {
// 没有 then 方法的话,直接把自身作为一个值
handleResult(item, i);
}
});
});
};

实现 race 方法

Promise.race() 方法也会对一组 Promise 进行处理,不过返回的是最先被处理的,可能是 resolve,也可能是 reject。

Promise.race = (promises) => {
return new Promise((resolve, reject) => {
promises.forEach(item => {
if (typeof item.then === 'function') {
item.then(resolve, reject);
} else {
resolve(item);
}
});
});
};

好了,一个完整的 Promise 我们已经完成了,通过自己实现一个 Promise 我们熟悉了 Promise 的工作原理,对 Promise 也有了更深一层的理解。

完整的代码已经托管在 GitHub 上,仓库地址:https://github.com/wangchi/sopromise

相关参考