书中第四章异步编程介绍的内容比较丰富,今天读到支持序列执行的Promise一节,为加深理解,将代码和体会整理如下。
让Promise支持序列执行的初衷是减少编码量。如书中所言,直接使用回调函数嵌套或者事件监听(Emitter)会导致大量的重复或者丑陋代码。而Promise有then方法,如果级联起来,代码逻辑清楚,内容简洁。
首先,定义Promise和Deferred对象:
var Promise = function(){
this.thenQueue = [];
this.isPromise = true;
};
var Deferred = function(){
this.promise = new Promise();
};
这里的thenQueue用于保存then方法中的回调函数,以便在后面序列执行。然后定义Promise的then方法:
Promise.prototype.then = function(fulfilledHandler){
var handler = {};
if(typeof fulfilledHandler === 'function'){
handler.fulfilled = fulfilledHandler;
}
this.thenQueue.push(handler);//放入回调函数队列
return this;//返回自身用于链式调用
};
为了简单起见,这里只实现了完成态的情况,不考虑出错失败态的情况。then方法返回Promise自身用于链式调用。然后定义Deferred的生成callback的工具函数:
Deferred.prototype.callback = function(){
var that = this;
return function(value){
that.resolve(value);
};
}
该函数为异步调用生成一个回调函数,该回调会触发Deferred的resolve方法,同样未考虑reject的情况。最后定义Deferred的resolve方法:
Deferred.prototype.resolve = function(val){
var promise = this.promise;
var handler;
while((handler = promise.thenQueue.shift())){
if(handler && handler.fulfilled){
var res = handler.fulfilled(val);
if(res && res.isPromise){
res.thenQueue = promise.thenQueue;//将queue中剩余回调转交给后续promise
this.promise = res;
return;
}
}
}
};
while循环从当前deferred对象的promise维护的回调队列中按顺序取出回调函数并执行,如果回调函数的返回值仍然是Promise,则将当前thenQueue赋值给该返回Promise,并将当前Deferred的promise对象更新为返回的Promise(为后续resolve做准备),然后立即返回(因为要按序执行,刚刚返回的promise还没有resolve,不能继续处理后续then的回调)。如果回掉函数的返回值不是Promise,则继续处理后续的then回调函数。下面举个例子:
var getValue = function(name, callback){
var map = {
'cat': 'july',
'july': 'jay'
};
setTimeout(function(){
callback(map[name]);
}, 500);
};
首先定义一个函数模拟异步调用,该函数通过一个name获取一个value,500毫秒返回结果。然后定义两个函数调用getValue方法,要求第二个函数调用依赖于第一个函数调用的返回结果,如:
var testValue1 = function(name){
var deferred = new Deferred();
getValue(name, deferred.callback());
return deferred.promise;
};
var testValue2 = function(name){
var deferred = new Deferred();
getValue(name, deferred.callback());
return deferred.promise;
};
testValue1('cat').then(function(value1){
//第二次调用依赖于第一次的结果
return testValue2(value1);
}).then(function(value2){
//输出'jay'
console.log(value2);
}).then(function(){
//该调用不依赖于前面结果,但也需等到前面调用完毕才能执行
console.log('Hello Cat');
});
testValue1异步调用结束后获得value1,然后调用testValue2,并传入value1。testValue2返回一个Promise,并进一步返回给resolve方法中的res变量,按照前面所述逻辑,此时testValue2放回的Promise维护的队列更新为新的队列(里面除去第一个then回调,还有两个函数待执行)。当第二个getValue结束后,输出结果'jay',没有返回值。因此在resolve方法中res的值是undefined,while循环会继续执行,取出最后一个then回调,执行之输出'Hello Cat',此时仍无返回值,且队列为空,程序结束。
另外,虽然后两次then回调没有返回Promise,但是如果后续调用依赖于testValue2的结果value2,仍然可以继续调用then方法,如:
testValue1('cat').then(function(value1){
//第二次调用依赖于第一次的结果
return testValue2(value1);
}).then(function(value2){
//输出'jay'
console.log(value2);
}).then(function(){
//该调用不依赖于前面结果,但也需等到前面调用完毕才能执行
console.log('Hello Cat');
}).then(function(value2){
//仍然输出'jay'
console.log(value2);
});
可以这样的做的原因是当前while循环仍然是resolve value2时的循环,依次在执行回调,value2依然可见。因此,如果后续调用不返回新的Promise,则一直可以依赖value2。而value1已经不可见,这是由调用的依赖关系决定的。
这种模式的好处是,简单明了地定义了多个异步调用顺序执行的逻辑,但即使两个调用之间没有依赖关系,仍然是顺序执行(如后两个then回调)。