Promise在日常开发中使用非常广泛,得益于其灵活的异步操作处理机制,我们对异步操作(尤其是具有依赖关系的异步操作)的处理大为简化。而了解其底层运行机制将有助于我们更灵活的使用Promise。本文旨在记录/总结我在实现Promise过程中的思路和个人理解。其中上篇介绍Promise基本功能与then方法的实现,下篇介绍其他实例方法与静态方法的实现。
在开始之前首先要说明几点
1. 本文适合对Promise有一定了解且有使用经验的小伙伴食用。关于Promise的基本使用我在前面有过介绍。
2. 我们本次实现的Promise是完全按照PromiseA+规范来实现的。不了解PromiseA+规范的小伙伴可以先参考一下。ES6中的Promise就采用了该规范。
3. 我们的总体思路是,首先回顾在日常开发中,Promise的某个功能点是如何使用的。进一步思考如何实现,做到有的放矢。
源码地址,欢迎star
一. 搭建初始结构
我们首先来搭建初始结构,在使用promise时,首先要在其构造函数传入executor函数,我们称之为执行器函数。执行器函数接收两个参数resolve,reject,这同样是两个函数,我们用它们来改变Promise的状态和结果。执行器函数同步执行,若执行过程中抛出错误,则promise立即变为失败状态。
而promise的状态有三种,分别是pending(等待),fulfilled(成功),rejected(失败)。状态只能从等待转变为成功/失败,且只能改变一次。
因此实现思路也就有了
- Promise构造函数中需要传入一个executor函数,默认立即同步执行,若执行中抛出错误,立即执行reject()。
- Promise内部提供两个方法 resolve(成功)、reject(失败) ,可以更改Promise的状态和结果。
- Promise有3个状态: (等待pending、成功fulfilled、失败rejected)。
我们按照上述搭建一下初始结构
// 声明Promise的三种状态
const Pending = "pending"; // 等待
const Fulfilled = "fulfilled"; // 成功
const Reject = "rejected"; // 失败
function Promise(executor) {
// 初始时为等待状态
this.PromiseState = Pending;
// 存储promise的结果
this.PromiseResult = null;
// 存储成功/失败的回调函数 后面会介绍如何使用
this.callbacks = [];
const resolve = () => {};
const reject = () => {};
// 同步执行执行器函数 若抛出错误 则执行reject()
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
二.实现resolve/reject
我们知道resolve/reject的职责有两点
- 改变Promise的状态
- 将传入方法的值设置为Promise的结果。
我们据此来实现
const resolve = (data) => {
// 要注意 Promise的状态只能修改一次 因此一旦发现Promise的状态已经改变 就不再继续向下执行
if (this.PromiseState !== Pending) return;
// 修改状态
this.PromiseState = Fulfilled;
// 设置结果值
this.PromiseResult = data;
};
const reject = (data) => {
if (this.PromiseState !== Pending) return;
this.PromiseState = Reject;
this.PromiseResult = data;
};
三.添加实例方法 then
then方法的功能以及实现比较复杂,我们分几个步骤进行。
1.添加对回调函数的处理
首先,我们使用then方法时,通常会传入两个回调函数(当然也可以只指定其中一个或都不指定),分别是Promise成功/失败后的回调。
而then的职责就是根据Promise的状态执行对应的回调函数(这样说其实是不准确的,后面会解释)。
Promise.prototype.then = function(onResolved, onRejected){
//根据promise的状态 执行相应的回调函数
if(this.PromiseState === Fulfilled){
// 调用回调函数时,传入promise的结果,也即调用resolve/reject时传入的参数
onResolved(this.PromiseResult);
}
if(this.PromiseState === Reject){
onRejected(this.PromiseResult);
}
}
2.完善then方法,添加对异步任务的回调处理。
目前实现的then方法存在缺陷。它只能解决同步调用resolve/reject的情况。我们来捋一下。
我们知道resolve/reject方法会改变Promise的状态,因此当resolve/reject同步执行(即执行器函数中进行的是同步操作)时,会导致执行then方法时resolve/reject已经执行完毕,即此时Promise一定已经改变,可以顺利执行then指定的回调。
但当resolve/rejetc异步调用,换句话说,我们在执行器函数中进行的是异步操作。这会导致resolve/rejetc的调用操作会进入任务队列。因此当执行then方法时,Promise的状态没有改变。仍然是'pending'。而我们知道,成功/失败的回调函数是一定要等到Promise的状态改变后再执行的。怎么办呢?
这时候,我们在Promise构造函数中声明的callbacks数组就派上了用场。我们可以在then方法中判断,当状态为'pending'时,将成功/失败的回调推入该数组中。等将来Promise状态改变时(也就是resolve/reject调用时)再取出来调用。因此我们也要进一步完善resolve/reject方法,使其能够在callbacks中存有回调时,循环调用。
为什么callbacks是个数组呢,这样可以允许我们指定多个then方法。
首先完善then方法
Promise.prototype.then = function(onResolved, onRejected){
//根据promise的状态 执行相应的回调函数
if(this.PromiseState === Fulfilled){
// 调用回调函数时,传入promise的结果,也即调用resolve/reject时传入的参数
onResolved(this.PromiseResult);
}
if(this.PromiseState === Reject){
onRejected(this.PromiseResult);
}
//pending状态时,暂存回调函数
if(this.PromiseState === Pending){
this.callbacks.push({
onResolved,
onRejected
})
}
}
完善reject/resolve
const resolve = (data) => {
if (this.PromiseState !== Pending) return;
this.PromiseState = Fulfilled;
this.PromiseResult = data;
// 判断是否有暂存的回调函数 有则循环调用
if (this.callbacks.length > 0) {
this.callbacks.forEach((cb) => cb.onResolved(data));
}
};
const reject = (data) => {
if (this.PromiseState !== Pending) return;
this.PromiseState = Reject;
this.PromiseResult = data;
if (this.callbacks.length) {
this.callbacks.forEach((cb) => cb.onRejected(data));
}
};
3.继续完善then。
在A+规范中约定,Promise的then方法会返回一个Promise,且该Promise的状态由then方法中指定的回调函数的返回结果决定。经过上面的讨论我们知道,then中的回调执行要分同步与异步两种情况。同样,我们这里也分开讨论。
同步的情况比较简单,由于调用then时,Promise的状态已经改变,因此我们只需调用相应的回调函数并拿到其执行结果。根据结果来决定then方法返回的Promise的状态即可。
Promise.prototype.then = function (onResolved, onRejected) {
// 创建一个新的Promise 最后返回它
let promise = new Promise((resolve, reject) => {
if (this.PromiseState === Fulfilled) {
try {
// 拿到回调的执行结果
let res = onResolved(this.PromiseResult);
// 若结果是promise 则then的状态和结果由该promise决定
// 这里的判断条件并不严苛 后面会继续完善
if (res instanceof Promise) {
// 若返回结果是Promise 则一定可以执行then方法
// 我们从then方法中获取回调返回的Promise的状态和结果,将其作为then的状态和结果
res.then(
(v) => {
resolve(v);
},
(r) => {
reject(r);
}
);
} else {
// 若是普通值则直接返回成功的promise并将该值作为结果值
resolve(res);
}
// 执行过程中抛出错误则直接返回失败的promise
} catch (e) {
reject(e);
}
} else if (this.PromiseState === Reject) {
try {
let res = onRejected(this.PromiseResult);
if (res instanceof Promise) {
res.then(
(v) => {
resolve(v);
},
(r) => {
reject(r);
}
);
} else {
resolve(res);
}
} catch (e) {
reject(e);
}
}
});
// 返回该promise
return promise;
};
接下来讨论异步修改Promise状态时,then返回的Promise的状态和结果问题。我们知道异步修改状态时,成功/失败的回调函数不是在then方法中直接执行。而是会暂存起来,在resolve/reject中执行。而这两个方法是在Promise构造函数中声明的。我们如何才能在构造函数中改变实例方法then的状态呢?这就需要在then暂存回调函数的操作中为回调函数绑定执行上下文。
// 我们把根据回调结果决定then的返回状态的操作先简单封装一下 后面会继续完善
function resolvePromise(result, resolve, reject) {
try {
if (result instanceof Promise) {
result.then(
(v) => {
resolve(v);
},
(r) => {
reject(r);
}
);
} else {
// 若是普通值则直接返回成功的Promise并将该值作为结果值
resolve(result);
}
} catch (e) {
reject(e);
}
}
Promise.prototype.then = function (onResolved, onRejected) {
let promise = new Promise((resolve, reject) => {
if (this.PromiseState === Fulfilled) {
try {
let res = onResolved(this.PromiseResult)
resolvePromise(res, resolve, reject);
} catch (e) {
reject(e);
}
} else if (this.PromiseState === Reject) {
try {
let res = onRejected(this.PromiseResult);
resolvePromise(res, resolve, reject);
} catch (e) {
reject(e);
}
} else {
this.callbacks.push({
// 对异步操作的回调处理 其行为与上面的同步操作的回调处理行为一致 只是要绑定上下文 否则将来执行时会丢失this
onResolved: function () {
try {
let res = onResolved(this.PromiseResult);
resolvePromise(res, resolve, reject);
} catch (e) {
reject(e);
}
}.bind(this), // 绑定上下文
onRejected: function () {
try {
let res = onRejected(this.PromiseResult);
resolvePromise(res, resolve, reject);
} catch (e) {
reject(e);
}
}.bind(this),
});
}
});
// 返回该promise
return promise;
};
四.添加catch方法。
catch方法主要用来捕获错误,而该方法的一大特性是能够捕获穿透的异常。也就是能捕获在任一阶段抛出的异常。因此我们要解决两个问题
1.捕获错误并执行错误回调。
这一点比较好实现,catch方法实质上就是特殊的then方法,我们只需要指定失败的回调函数即可。
2.实现异常穿透。
我们首先要了解穿透的意义是什么,即当链式调用的某个节点抛出了异常,但没指定相应的失败回调,则该错误信息会一直向下传递,直到被catch方法捕获。
而实现穿透的关键在于,如何实现在没指定回调函数的情况下,将状态和结果向下传递。
因此我们要指定回调的默认行为。
默认行为的职责就是将错误传递下去。因为既然要穿透,说明我们没有为前面的错误指定回调。因此才要将错误向下传递,让后面的错误回调来捕获到该错误。因此默认回调的行为也就是将错误信息传递下去。如何传递呢?试想一下
既然要调用错误的回调,说明上一个Promise对象状态为失败了。因此默认回调就是要让它一直错下去!怎么办? 使用throw抛出错误。
我们知道,在then的链式调用过程中,then返回的Promise的状态和结果是由then的回调的返回结果决定的。因此若在默认的失败回调中抛出了错误,则会立即被trycatch捕获到。因此当前的then的返回结果会立即变为失败的Promise且结果是抛出的错误信息。再进一步,由于当前then的返回了失败的Promise,因此下个then一定会执行其失败回调。若下个then指定了失败回调,则前面的错误就被捕获到了。若仍然没指定失败回调,则又会执行默认的失败回调。由此就达到了异常穿透的效果。假设我们在then的链式调用过程中一直没指定失败回调,则最终抛出的错误就会被catch方法捕获。
同理,成功的状态和结果也可以传递,也就是我们在then的链式调用过程中,即使没有为中间的某个then指定回调函数也不会中断链式调用。其状态和结果会继续向下传递。
接下来实现catch方法和指定then方法的默认回调行为。
Promise.prototype.catch = function (onRejected) {
// 直接调用then,不传成功的回调
return this.then(undefined, onRejected);
};
Promise.prototype.then = function (onResolved, onRejected) {
if (typeof onRejected !== "function") {
onRejected = (reason) => {
// 抛出异常这将使得下个then继续执行失败回调
throw reason;
};
}
if (typeof onResolved !== "function") {
// 返会成功信息 下个then会执行成功回调
onResolved = (value) => value;
}
......
};
五.异步执行回调
这里要说明一点,我们一般认为Promise的then方法是异步执行的,而且在日常使用中Promise的then方法的行为似乎也印证了这一点。但实际上真正异步执行的是then方法指定的回调函数。可是then方法的职责不就是根据Promise的状态来执行相应的回调吗?事实上经过前面的then方法的实现我们已经知道,then方法本身是同步执行的,当执行then时,若Promise状态已经改变,则会执行回调。若未改变则会将回调暂存。由此可见回调的执行不一定是在then方法中,因此我们说前面对then方法职责的阐述是有有缺陷的。因此要实现回调的异步执行我们不能从then方法下手,而是应对回调函数本身动手脚。实现异步执行的方式有很多,这里就用定时器实现。
//这里就以执行成功的回调为例,我们只需包一层定时器即可。
......
if (this.PromiseState === Fulfilled) {
setTimeout(() => {
try {
let x = onResolved(this.PromiseResult);
resolvePromise(res, resolve, reject);
} catch (e) {
reject(e);
}
});
}
......
六.细节问题
至此Promise的基本功能已经完成,接下来完善几个细节问题
1.then方法中成功/失败的回调的返回值问题。具体如下
当回调的返回值与当前的then方法的返回值引用了同一个promise对象时,会造成死循环,因此应抛出错误。
接下来就是具体判断返回值是不是Promise。我们之前用的instanceof方法不能最准确的判断。由于该回调函数的返回值直接决定了then的状态和结构,因此我们要严格判断它是不是Promise。按照PromiseA+规范,只有当返回值的类型是对象或函数,存在then属性,且then属性是函数时这样才能保证返回值它是Promise。同时还要保证,若resolve/reject同时被调用或被调用多次,只取第一次,其他调用会被忽略。
当resolve/reject返回的仍然是Promise,则递归解析直到为普通值。这块逻辑具体可以参考A+规范文档中对该部分的阐述。
下面来完善resolvePromise方法
function resolvePromise(promise, res, resolve, reject) {
// 1.回调的返回值和then的返回值不能引用同一个对象 可能造成死循环
if (promise === res) {
return reject(new TypeError("不能引用同一个对象"));
}
// 该变量为已经调用回调的标志,避免多次调用。
let called;
// 2.res是对象或者函数,说明有可能是promise
if ((typeof res === "object" && res != null) || typeof res === "function") {
try {
let then = res.then; // 获取其then属性
// 存在then属性,且是函数类型,则可以断定是promise
if (typeof then === "function") {
// 调用then并绑定上下文
then.call(
res,
(y) => {
// 避免多次调用
if (called) return;
called = true;
// 若返回值仍是promise 则递归解析直到为普通值
resolvePromise(promise, y, resolve, reject);
},
(r) => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(res);
}
} catch (e) {
// 若取then或执行then的过程中出错,直接返回失败。
if (called) return;
called = true;
reject(e);
}
} else {
resolve(res);
}
}
由于修改了resolvePromise,因此调用该方法的地方也要做出调整。
......
if (this.PromiseState === Fulfilled) {
setTimeout(() => {
try {
let res = onResolved(this.PromiseResult);
//将当前then方法即将返回的Promise传入
resolvePromise(promise, res, resolve, reject);
} catch (e) {
reject(e);
}
});
}
......
2 resolve/reject中传入的仍是Promise。
上面分析过程中有类似的情况,解决办法就是递归解析直到为普通值。
......
let resolve = (value) => {
// 增加判断如果resolve传入的是promise的判断
// 这里无需进行像上面那样苛刻的判断,我们要的只是他的返回值
if (value instanceof Promise) { // 递归解析直到为普通值为止
value.then(resolve, reject)
return
}
}
......
七.测试
至此Promise的基本功能已经实现。
我们可以用promises-aplus-tests这款插件来测试我们写的promise符不符合A+规范。
分为三步
1 全局安装 npm i -g promises-aplus-tests
2 在我们写的promise.js文件中配置脚本
......
Promise.defer = Promise.deferred = function () {
let dfd = {}
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}
module.exports = Promise;
- 3 运行文件测试 promises-aplus-tests promise.js
只要通过全部测试,则说明我们写的Promise是符合PromiseA+规范的,如下图所示。
以上就是符合A+规范的Promise的实现过程,在下篇中将继续完成Promise的其他实例方法和静态方法。
参考:https://github.com/Tie-Dan/Promise/blob/master/code/2.promise/promise6.js