前言
Promise是异步操作中最常用的一种模式,这篇文章我们探讨一下如何基于Promise/A规范实现一个Promise,完整实现采用ES5语法,运行环境为Node.js,使用process.nextTick实现推迟then指定的回调函数的执行。
简化后的Promise/A规范
查阅了一下Promise/A规范,总结出来了一下几个要求,Promise的功能基于以下几个要求来实现
- 使用new Promise创建一个对象,参数为一个方法,传入的方法有两个参数
- Promise.then方法为指定的Promise回调之后调用的方法,这个参数方法可以有一个参数,也可以有两个参数
- Promise.catch方法指定失败回调
- Promise.then和Promise.catch的返回值都是一个Promise,如果在回调函数中指定了返回的Promise,则返回这个promise,否则返回一个新的状态为 resolved的promise
- Promise有三种状态,分别是penging、resolved、rejected,如果状态改变成resolved或者rejected则无法再发生变化
- 使用Promise.then指定的回调函数,回调函数会在当前的同步代码执行完成后执行,如果Promise的状态已经是resolved,则回调也会推迟到下一次微事件循环时执行
- 同一个promise对象可以指定多个回调函数,这些回调函数的执行顺序按照声明的时候的顺序来执行
开始实现
根据Promise的使用经验,我们很容易的给出如下的代码结构:
function MyPromise(fn){
// 私有方法
function resolve(value){}
function reject(value){}
// 公有方法
this.then = function(fn){}
this.catch = function(fn){}
this.finally = function(fn){}
fn.call(global, resolve, reject); // 调用fn方法,并将resolve和reject作为参数传进去
}
然后MyPromise有三种状态,和一个值,添加这两个私有变量。然后同一个promise可以添加多个then、catch回调,这是我们需要用一个回调函数队列来实现。
改变后的代码结构如下:
function MyPromise(fn){
// 私有属性
var __status = 'pending';
var __data;
var resolveCallbacks = new Array();
var rejectCallbacks = new Array();
var finallyCallback;
// 私有方法
function resolve(value){}
function reject(value){}
// 公有方法
this.then = function(fn){}
this.catch = function(fn){}
this.finally = function(fn){}
fn.call(global, resolve, reject); // 调用fn方法,并将resolve和reject作为参数传进去
}
接下来就可以来实现我们的逻辑了,整个的过程思路是,在调用resolve方法之后,MyPromise的状态变成fulfilled,然后异步执行resolveCallbacks中注册的回调函数。调用reject方法也执行类型的操作。然后多次调用resolve或者reject是无效的。
在then方法中我们可以判断当前Promise的状态,然后执行不同的操作
代码如下:
function MyPromise(fn){
// 私有属性
var __status = 'pending';
var __data;
var resolveCallbacks = new Array();
var rejectCallbacks = new Array();
var finallyCallback;
// 私有方法
function resolve(value){
if(___status !== 'pending') return;
__status = 'resolved';
__data = value;
exec(resolveCallbacks);
}
function reject(value){
if(___status !== 'pending') return;
__status = 'rejected';
__data = value;
exec(rejectCallbacks);
}
// 这个方法在后面解释
function exec(callbacks){
while(callbacks.length > 0){
var head = callbacks.shift();
(function _temp(head){ // 利用匿名立即执行函数解决异步操作中的引用问题
process.nextTick(function(){
var result = head.f.call(global, __data);
if(result instanceof MyPromise) result.then(function(d){
head.r.call(global, d);
})
else head.r.call(global,result);
})
})(head)
}
if(finallyCallback) finallyCallback.call(global);
}
// 公有方法
this.then = function(fn){
var tempResolve, tempReject;
var retPromise = new MyPromise(function(resolve, reject){
tempResolve = resolve; // 拿到新的MyPromise对象的resolve方法引用
tempReject = reject;
})
if(__status == 'resolved') {
process.nextTick(function(){fn.call(global,__data);})
}else if(__status == 'pending')
resolveCallbacks.push({f:fn, r: tempResolve});
if(arguments.length == 2){
if(__status == 'rejected') {
process.nextTick(function(){fn.call(global,__data);})
}else if(__status == 'pending')
rejectCallbacks.push({f:fn, r: tempReject});
}
return retPromise;
}
this.catch = function(fn){
var tempReject;
var retPromise = new MyPromise(function(resolve, reject){
tempReject = reject;
})
if(__status == 'rejected') {
process.nextTick(function(){fn.call(global,__data);})
}else if(__status == 'pending')
rejectCallbacks.push({f:fn, r: tempReject});
return retPromise;
}
this.finally = function(fn){
if(__status == 'pending') process.nextTick(function(){fn.call(global)})
else finallyCallback = fn;
}
fn.call(global, resolve, reject); // 调用fn方法,并将resolve和reject作为参数传进去
}
关键实现思路
给出以下几个关键问题的解决方案:
- then回调函数是延迟执行的,那么我们怎么知道回调函数返回的是不是MyPromise呢?
解决方案:
call函数的返回值是回调函数调用后的返回值。 - 链式then调用的问题怎么解决呢?
解决方案:
在then函数中拿到新返回的Promise对象的resolve/reject函数的引用,因为两个函数可以改变MyPromise对象的状态,然后将这个函数和回调函数一起入回调队列,这样就可以实现根据then参数返回值决定then返回的MyPromise的状态了,几处核心代码:// 1. var tempResolve, tempReject; var retPromise = new MyPromise(function(resolve, reject){ tempResolve = resolve; // 拿到新的MyPromise对象的resolve方法引用 tempReject = reject; }) // 2. rejectCallbacks.push({f:fn, r: tempReject}); //3. var result = head.f.call(global, __data); if(result instanceof MyPromise) result.then(function(d){ head.r.call(global, d); }) else head.r.call(global,result);
MyPromise测试
测试代码如下:
var promise = new MyPromise(function(resolve, reject) {
setTimeout(() => {
resolve("data");
console.log("下一次宏任务中的事务");
}, 400);
});
promise.then(function(data) {
console.log(data + 1);
});
promise.then(data => {
console.log(data + 2);
});
promise.then(data => {
console.log(data + 3);
});
promise
.then(data => {
console.log(data + 4);
return new MyPromise(function(resolve, reject) {
setTimeout(() => resolve(data + "这是下一个promise的操作"), 1000);
});
})
.then(res => console.log(res));
promise.then(function(data) {
console.log(data + 5);
});
console.log("同步代码");
输出的结果如下
同步代码
下一次宏任务中的事务
data1
data2
data3
data4
data5
data这是下一个promise的操作