从现在开始,我们就来动手实现一个功能完整的Promise,一步步深挖其中的细节。我们先从链式调用开始。
简易版实现
首先写出第一版的代码:
//定义三种状态const PENDING = "pending";const FULFILLED = "fulfilled";const REJECTED = "rejected";function MyPromise(executor) {let self = this; // 缓存当前promise实例self.value = null;self.error = null;self.status = PENDING;self.onFulfilled = null; //成功的回调函数self.onRejected = null; //失败的回调函数const resolve = (value) => {if(self.status !== PENDING) return;setTimeout(() => {self.status = FULFILLED;self.value = value;self.onFulfilled(self.value);//resolve时执行成功回调});};const reject = (error) => {if(self.status !== PENDING) return;setTimeout(() => {self.status = REJECTED;self.error = error;self.onRejected(self.error);//resolve时执行成功回调});};executor(resolve, reject);}MyPromise.prototype.then = function(onFulfilled, onRejected) {if (this.status === PENDING) {this.onFulfilled = onFulfilled;this.onRejected = onRejected;} else if (this.status === FULFILLED) {//如果状态是fulfilled,直接执行成功回调,并将成功值传入onFulfilled(this.value)} else {//如果状态是rejected,直接执行失败回调,并将失败原因传入onRejected(this.error)}return this;}
可以看到,Promise 的本质是一个有限状态机,存在三种状态:
- PENDING(等待)
- FULFILLED(成功)
- REJECTED(失败)
状态改变规则如下图:
对于 Promise 而言,状态的改变不可逆,即由等待态变为其他的状态后,就无法再改变了。
不过,回到目前这一版的 Promise, 还是存在一些问题的。
设置回调数组
首先只能执行一个回调函数,对于多个回调的绑定就无能为力,比如下面这样:
let promise1 = new MyPromise((resolve, reject) => {fs.readFile('./001.txt', (err, data) => {if(!err){resolve(data);}else {reject(err);}})});let x1 = promise1.then(data => {console.log("第一次展示", data.toString());});let x2 = promise1.then(data => {console.log("第二次展示", data.toString());});let x3 = promise1.then(data => {console.log("第三次展示", data.toString());});
这里我绑定了三个回调,想要在 resolve() 之后一起执行,那怎么办呢?
需要将 onFulfilled 和 onRejected 改为数组,调用 resolve 时将其中的方法拿出来一一执行即可。
self.onFulfilledCallback = [];self.onRejectedCallback = [];
MyPromise.prototype.then = function(onFulfilled, onRejected) {if (this.status === PENDING) {this.onFulfilledCallbacks.push(onFulfilled);this.onRejectedCallbacks.push(onRejected);} else if (this.status === FULFILLED) {onFulfilled(this.value);} else {onRejected(this.error);}return this;}
接下来将 resolve 和 reject 方法中执行回调的部分进行修改:
// resolve 中self.onFulfilledCallback.forEach((callback) => callback(self.value));//reject 中self.onRejectedCallback.forEach((callback) => callback(self.error));
链式调用完成
我们采用目前的代码来进行测试:
let fs = require('fs');let readFilePromise = (filename) => {return new MyPromise((resolve, reject) => {fs.readFile(filename, (err, data) => {if(!err){resolve(data);}else {reject(err);}})})}readFilePromise('./001.txt').then(data => {console.log(data.toString());return readFilePromise('./002.txt');}).then(data => {console.log(data.toString());})// 001.txt的内容// 001.txt的内容
咦?怎么打印了两个 001,第二次不是读的 002 文件吗?
问题出在这里:
MyPromise.prototype.then = function(onFulfilled, onRejected) {//...return this;}
这么写每次返回的都是第一个 Promise。then 函数当中返回的第二个 Promise 直接被无视了!
说明 then 当中的实现还需要改进, 我们现在需要对 then 中返回值重视起来。
MyPromise.prototype.then = function (onFulfilled, onRejected) {let bridgePromise;let self = this;if (self.status === PENDING) {return bridgePromise = new MyPromise((resolve, reject) => {self.onFulfilledCallbacks.push((value) => {try {// 看到了吗?要拿到 then 中回调返回的结果。let x = onFulfilled(value);resolve(x);} catch (e) {reject(e);}});self.onRejectedCallbacks.push((error) => {try {let x = onRejected(error);resolve(x);} catch (e) {reject(e);}});});}//...}
假若当前状态为 PENDING,将回调数组中添加如上的函数,当 Promise 状态变化后,会遍历相应回调数组并执行回调。
但是这段程度还是存在一些问题:
- 首先 then 中的两个参数不传的情况并没有处理,
- 假如 then 中的回调执行后返回的结果(也就是上面的
x)是一个 Promise, 直接给 resolve 了,这是我们不希望看到的。
怎么来解决这两个问题呢?
先对参数不传的情况做判断:
// 成功回调不传给它一个默认函数onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;// 对于失败回调直接抛错onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };
然后对返回Promise的情况进行处理:
function resolvePromise(bridgePromise, x, resolve, reject) {//如果x是一个promiseif (x instanceof MyPromise) {// 拆解这个 promise ,直到返回值不为 promise 为止if (x.status === PENDING) {x.then(y => {resolvePromise(bridgePromise, y, resolve, reject);}, error => {reject(error);});} else {x.then(resolve, reject);}} else {// 非 Promise 的话直接 resolve 即可resolve(x);}}
然后在 then 的方法实现中作如下修改:
resolve(x) -> resolvePromise(bridgePromise, x, resolve, reject);
在这里大家好好体会一下拆解 Promise 的过程,其实不难理解,我要强调的是其中的递归调用始终传入的resolve和reject这两个参数是什么含义,其实他们控制的是最开始传入的bridgePromise的状态,这一点非常重要。
紧接着,我们实现一下当 Promise 状态不为 PENDING 时的逻辑。
成功状态下调用then:
if (self.status === FULFILLED) {return bridgePromise = new MyPromise((resolve, reject) => {try {// 状态变为成功,会有相应的 self.valuelet x = onFulfilled(self.value);// 暂时可以理解为 resolve(x),后面具体实现中有拆解的过程resolvePromise(bridgePromise, x, resolve, reject);} catch (e) {reject(e);}})}
失败状态下调用then:
if (self.status === REJECTED) {return bridgePromise = new MyPromise((resolve, reject) => {try {// 状态变为失败,会有相应的 self.errorlet x = onRejected(self.error);resolvePromise(bridgePromise, x, resolve, reject);} catch (e) {reject(e);}});}
Promise A+中规定成功和失败的回调都是微任务,由于浏览器中 JS 触碰不到底层微任务的分配,可以直接拿 setTimeout(属于宏任务的范畴) 来模拟,用 setTimeout将需要执行的任务包裹 ,当然,上面的 resolve 实现也是同理, 大家注意一下即可,其实并不是真正的微任务。
if (self.status === FULFILLED) {return bridgePromise = new MyPromise((resolve, reject) => {setTimeout(() => {//...})}
if (self.status === REJECTED) {return bridgePromise = new MyPromise((resolve, reject) => {setTimeout(() => {//...})}
好了,到这里, 我们基本实现了 then 方法,现在我们拿刚刚的测试代码做一下测试, 依次打印如下:
001.txt的内容002.txt的内容
可以看到,已经可以顺利地完成链式调用。
错误捕获及冒泡机制分析
现在来实现 catch 方法:
Promise.prototype.catch = function (onRejected) {return this.then(null, onRejected);}
对,就是这么几行,catch 原本就是 then 方法的语法糖。
相比于实现来讲,更重要的是理解其中错误冒泡的机制,即中途一旦发生错误,可以在最后用 catch 捕获错误。
我们回顾一下 Promise 的运作流程也不难理解,贴上一行关键的代码:
// then 的实现中onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };
一旦其中有一个PENDING状态的 Promise 出现错误后状态必然会变为失败, 然后执行 onRejected函数,而这个 onRejected 执行又会抛错,把新的 Promise 状态变为失败,新的 Promise 状态变为失败后又会执行onRejected……就这样一直抛下去,直到用catch 捕获到这个错误,才停止往下抛。
这就是 Promise 的错误冒泡机制。
至此,Promise 三大法宝: 回调函数延迟绑定、回调返回值穿透和错误冒泡。
