- Promise 基本结构
我们知道实例化Promise对象时传入一个函数作为执行器,有两个参数(resolve和reject)分别将结果变为成功态和失败态。
据此我们可以写出基本结构:
function Promise(excutor){
let self = this;
self.status = 'pending'; // 状态 等待 => 成功或失败
self.value = null; // 成功结果
self.reason = null; // 失败原因
function resolve(value){
};
function reject(reason){
};
}
- then方法
每一个Promise实例都有一个then方法,它用来处理异步返回的结果,它是定义在原型上的方法。
// 一个成功 一个失败
Promise.prototype.then = function(onFulfilled, onRejected){
}
当我们自己实例化一个Promise时,其执行器函数(executor)会立即执行,这是一定的:
let demo = new Promise((resolve, reject)=>{
console.log("在座的各位彭于晏,大家好");
})
因此,当实例化Promise时,构造函数中就要马上调用传入的excutor函数执行,为了防止出错,加入try,catch:
// 此段代码放在Promise方法里
try {
excutor(resolve, reject);
} catch(err) {
reject(err)
}
3. 状态管理
已经是成功态或是失败态不可再更新状态,Promise 规范中规定:当Promise对象已经由pending状态改变为了成功态(resolved)或是失败态(rejected)就不能再次更改状态。
因此我们在更新状态时要判断,如果当前状态是pending(等待态)才可更新。
function resolve(value){
if (self.status === 'pending') {
self.value = value; //保存成功结果
self.status = 'fulfilled';
}
}
function reject(reason){
if (self.status === 'pending') {
self.reason = reason;
self.status = 'rejected';
}
}
以上可以看到:
1. 在resolve和reject函数中分别加入了判断
2. 只有当前状态是pending才可进行操作
3. 将成功的结果和失败的原因都保存到对应的属性上。之后将state属性置为更新后的状态
- then方法完善
当Promise的状态发生了改变,不论是成功或是失败都会调用then方法。所以,then方法的实现也很简单,根据state状态来调用不同的回调函数即可。
Promise.prototype.then = function(onFulfilled, onRejected){
onFulfilled = typeof onFulfilled === 'function' ?
onFulfilled : function (data) { resolve(data) }
onRejected = typeof onRejected === 'function' ?
onRejected : function (err) { throw err }
}
代码写到这里似乎基本功能都实现了,可是还有一个很大的问题:目前此Promise还不支持异步代码,如果Promise中封装的是异步操作,then方法无能为力,例:
let demo = new Promise((resolve, reject)=>{
console.log("在座的各位彭于晏大家好");
setTimeout(()=>{
resolve(1);
}, 500)
})
// 没有打印
demo.then(data => console.log(data));
第一步是正常打印的,但是 setTimeout 里面的代码不会执行,最后一行也不会打印 “1”。
问题所在:原因是 setTimeout 函数使得 resolve 是异步执行的,有延迟,当调用then方法的时候,此时此刻的状态还是等待态(pending),因此then方法即没有调用 onFulfilled 也没有调用 onRejected。
- 发布订阅模式登场
这个问题如何解决?我们可以参照发布订阅模式,在执行 then 方法时如果还在等待态(pending),就把回调函数临时寄存到一个数组里,当状态发生改变时依次从数组中取出执行就好,是不是很Nice!
清楚这个思路我们开始实现它,首先在类上新增两个Array类型的数组,用于存放回调函数:
function Promise(excutor){
// 其它代码略
self.onFulfilledCallbacks = [];
self.onRejectedCallbacks = [];
}
在then方法里添加进去:
Promise.prototype.then = function(onFulfilled, onRejected){
// 其它代码略
let self = this;
if(self.status === "pending"){
self.onFulfilledCallbacks.push(onFulfilled);
self.onRejectedCallbacks.push(onRejected);
}
}
在resolve() 与reject() 里逐一遍历:
function resolve(value){
if (self.status === 'pending') {
// 其它代码略
self.onFulfilledCallbacks.forEach(item => item(value));
}
}
function reject(reason){
if (self.status === 'pending') {
// 其它代码略
self.onRejectedCallbacks.forEach(item => item(reason));
}
}
到这一步,异步也搞定了!再是如何实现链式调用。
6. 经典的链式调用实现
then方法接收的两个函数中,可以通过return把值传给下一个步,也可以返回一个新的Promise把值传给下一步,then方法执行的时候有个特点:
为了保证链式调用,上一次then中不管你是成功还是失败都会把参数作为下一个then中成功时回调的参数
话不多说,开干(只需要再new一个Promise即可):
Promise.prototype.then = function(onFulfilled, onRejected){
onFulfilled = typeof onFulfilled === 'function' ?
onFulfilled : function (data) { resolve(data) }
onRejected = typeof onRejected === 'function' ?
onRejected : function (err) { throw err }
let self = this;
if(self.status === "fulfilled"){
return new Promise((resolve, reject) => {
try{
let x = onFulfilled(self.value);
x instanceof Promise ? x.then(resolve, reject) : resolve(x);
} catch (err){
reject(err)
}
})
}
if(self.status === "rejected"){
return new Promise((resolve, reject) => {
try{
let x = onRejected(self.reason);
x instanceof Promise ? x.then(resolve, reject) : resolve(x);
} catch (err){
reject(err)
}
})
}
if(self.status === "pending"){
return new Promise((resolve, reject) => {
self.onFulfilledCallbacks.push(()=>{
let x = onFulfilled(self.value);
x instanceof Promise ? x.then(resolve, reject) : resolve(x);
})
self.onRejectedCallbacks.push(()=>{
let x = onRejected(self.reason);
x instanceof Promise ? x.then(resolve, reject) : resolve(x);
})
})
}
}
}
7. 最后的catch方法
其实catch方法就是then方法的简写
Promise.prototype.catch = function(fn){
return this.then(null, fn);
}
现在,一个完整的Promise就实现了,大家可以去试试。有啥不对的可以在评论区指出,感谢。
考虑到一些新手基础可能差点,本篇文章可以说讲解的非常非常详细了,拆分了多个小步骤,一步一步来。相信大家跟着思路走都是能搞定的。排版啥的也都尽量搞得好看些,提高各位客官的用户体验。