从现在开始,我们就来动手实现一个功能完整的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是一个promise
if (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.value
let 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.error
let 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 三大法宝: 回调函数延迟绑定
、回调返回值穿透
和错误冒泡
。