異步編程系列教程:
-
(翻譯)異步編程之Promise(1)——初見魅力
-
異步編程之Promise(2):探究原理
-
異步編程之Promise(3):拓展進階
-
異步編程之Generator(1)——領略魅力
-
異步編程之Generator(2)——剖析特性
- 異步編程之co——源碼分析
拓展功能
在前面的文章中,通過了解promise能做什么,實踐動手從原理上了解
promise/deferred
模式的用法,相信大家應該更期待這次的功能拓展。我們不僅需要讓單異步操作promise化,我們還需要從實際出發,拓展更多有用的功能。直接看一下我們這一次需要做的兩個功能:
-
多異步并行控制
- 多異步串行隊列
這兩個功能用我們之前自己寫的簡陋promise庫,是無法做到的。我們不能在指定多個promise異步完成后,再觸發回調。也不能讓多個promise異步像排隊一樣,一個一個的進行,甚至下一個promise的參數是依賴上一個promise的。這就是我們接下來需要解決的問題:
多異步并行控制
在凍手之前,我們先想一想大致的思路吧。首先我們肯定是并發了多個異步,我們需要做的僅僅就是,監控所有并發的異步,并讓最后一個異步觸發
resolve
回調函數。當然錯誤處理的話,就是當有一個異步錯誤,直接就
reject
掉宣布異步失敗結束。一般監視并發,我們都會有一個哨兵變量,每完成一個異步,就對哨兵進行維護并檢測異步是否結束。
那我們的API應該怎么設置呢?樸靈老師的書上是這樣的:
deferred.all([promise1, promise2]).then()
。從這里我們可以看出,就是由各個小promise組成了一個大的promise,并在大promise中進行接下來的操作。一起看一下代碼吧:
Deferred.prototype.all = function(promises){
var result = []; // 存儲各個promise的執行結果
var count = promises.length; // 哨兵變量
var _this = this;
promises.forEach(function(promise, index){
promise.then(function(res){
result[index] = res;
count--;
// 當執行最后一個promise后, 調用大promise的resolve,并把result傳進去
while(count === 0){
_this.resolve(result);
}
}, function(err){
// 有一個promise出錯,立即return并執行大promise的reject
return _this.reject(err);
});
});
return this.promise;
};
我個人認為最不好懂的應該是
_this
到底指的是什么?看過上一篇的朋友,應該知道deferred是延遲對象來的,作用就是觸發即將在
then()
中綁定的
resolve()
和
reject()
。那這里的
_this
必然是指大的promise,我們看一下如何使用的:
// 已經定義好Promise化的readFile(),不懂的同學可以翻閱上一篇文章。
// 這段代碼是輸出兩個文件里,字符串length最大的值。
var r1 = readFile("hello.txt", 'utf-8');
var r2 = readFile("hello2.txt", 'utf-8');
var deferred = new Deferred(); // 初始化一個延時對象。
deferred.all([r1, r2]).then(function(res){
console.log(res);
res = res.map(function(item){return item.length});
console.log(Math.max.apply(null, res));
});
That's easy, right?! 我們這里僅僅是實現原理,是不成熟的,若實際使用中,更推薦Q.js。現在我們將需要并行的promise放到一個數組里,不出錯就會得到每一次并行的結果,并存儲在
result
中,最后返回得到并進行相應處理。當然我們也可以很清楚感受到它的局限,并行的promise是相互獨立無依賴的。當多個異步開始有依賴了,我們該怎么做呢?這就是我們接下來要討論的。
多異步串行隊列
一般來說,多異步串行執行,通過最簡單的嵌套回調即可解決。但我們可以想象,我們最終的理想形態應該是鏈式結構的。
res
依賴以上的步驟,我們通過鏈式結構可以更清晰易懂,有助于我們進行流程控制。
--------嵌套回調---------
api1(function(v1){
api2(function(v1, v2){
api3(function(v2, v3){
api4(function(v3, res){
callback(res);
})
})
})
});
--------鏈式調用---------
promise()
.then(api1)
.then(api2)
.then(api3)
.then(function(res){
// 用res來做一些事情
})
還是從想開始,我們需要做到promise支持鏈式執行,第一感覺的數據結構就是隊列,就是那個FIFO先進先出的隊列。我們將所有的回調都壓入隊列中,完成一個就取一個出來執行。但是更關鍵的問題在于,前面一個promise的值,如何傳到下一個promise中。樸靈大大在這里給出的解決方案是: Promise執行回調時,一旦檢測到返回的是新的Promise對象,會將當前Deferred延遲對象中的promise引用換成新的Promise對象。而那個回調隊列,也同樣轉移到了新Promise上。
不知道大家有沒有聽懂大概個意思,如果還是不太清楚,我們可以思考一下,再對比一下實現的代碼,就應該能看懂了。這次我們需要對以往的代碼,做一個較大的改變,我們不再使用
events.EventEmitter
來進行事件觸發了。為了能鏈式的調用回調,我們會將事件觸發放在數組隊列里,并按順序進行觸發。因為代碼進行了較大的改變,我們逐個逐個看代碼。
var Promise = function(){
this.isPromise = true; // 用于確定是promise對象
this.queue = []; // 回調事件的隊列
};
Promise.prototype.then = function(resolve, reject){
var handler = {};
if(typeof resolve === 'function'){
handler.resolve = resolve;
}
if(typeof reject === 'function'){
handler.reject = reject;
}
this.queue.push(handler); // 將回調事件推入到數組隊列中
return this;
};
這一段代碼,我們最重要的是定義了一個
queue
屬性。它是用來存放在
then(resolve, reject)
中的
resolve
和
reject
方法的。最后我們會將一次promise的回調函數,推入到
queue
屬性里,以供deffered延遲對象使用。
var Deferred = function(){
this.promise = new Promise();
};
Deferred.prototype.resolve = function(data){
var handler; //用于存放當前的回調
// 若隊列存在回調
while(handler = this.promise.queue.shift()){
if(handler && handler.resolve){
var ret = handler.resolve(data);
if(ret && ret.isPromise){
ret.queue = this.promise.queue;
this.promise = ret;
return;
}
}
}
};
Deferred.prototype.reject = function(err){
var handler; //用于存放當前的回調
// 若隊列存在回調
while(handler = this.promise.queue.shift()){
if(handler && handler.reject){
var ret = handler.reject(err);
if(ret && ret.isPromise){
ret.queue = this.promise.queue;
this.promise = ret;
return;
}
}
}
};
Deferred.prototype.makeNodeResolver = function(){
var _this = this;
return function(err, res){
if(err) return _this.reject(err);
_this.resolve(res);
}
};
這里,和以往一樣,每一個deferred對象都會有一個promise對象。并且重新定義了
resolve
和
reject
的實現,不再和以往一樣,簡單的通過觸發事件實現。我們仔細分析一下,到底deffered對象的方法做了些什么。我們就取其中一個
resolve
來看,首先我們將隊列promise的回調隊列
queue
最前端的handler推出來,若存在就執行回調。若回調執行的結果是一個新的promise(我們通過isPromise屬性判斷),我們就會進行一個替換。這里是實現的關鍵,我們將原來那個promise的
queue
屬性存到新的新的promise上,然后將deferred對象當前的promise變成新的promise,最后返回出來。通過這一系列的操作,我們就可以將回調隊列進行傳遞,并實現鏈式調用。
--------hello.txt---------
data.json
--------data.json---------
{"message": "Hello World!"}
--------代碼應用---------
var fs = require('fs');
var readFile = function(file){
var deferred = new Deferred();
fs.readFile(file, 'utf-8', deferred.makeNodeResolver());
return deferred.promise;
};
var readJSON = function(file){
var deferred = new Deferred();
fs.readFile(file, 'utf-8', function(err, file){
if(err) return deferred.reject(err);
deferred.resolve(JSON.parse(file));
});
return deferred.promise;
};
readFile('hello.txt').then(function(file){
return readJSON(file);
}).then(function(data){
console.log(data.message);
});
// 或者利用更簡潔的特性
readFile('hello.txt').then(readJSON).then(function(data){
console.log(data.message); // hello world!
});
最后這段代碼是我們多異步并行隊列的實際應用。我們定義了兩個promise化的異步方法,一個是readFile,一個readJSON。我們的readJSON函數是依賴readFile的結果的,最后我們一樣實現了需求。我們這次也僅僅是研究原理實現的代碼,是不成熟的。在實際應用中,還是需要借助成熟的框架Q.js等。
API promise化的封裝
我們可以發現,為了使代碼實現promise,我們需要為現有的異步api都進行一次封裝。為了某些特殊情況,我們可以自己動手用promise/deferred模式,進行手動封裝實現功能。然后很多現有的API,我們是可以從中抽象出相同的部分,借助函數柯里化,進行批量promise轉化的。
var wrapPromise = function(api){
return function(){
var deferred = new Deferred();
var args = [].slice.call(arguments, 0);
args.push(deferred.makeNodeResolver());
api.apply(null, args);
return deferred.promise;
};
};
var fs = require('fs');
var readFile = wrapPromise(fs.readFile);
我們通過
wrapPromise(api)
,將實現的細節隱藏在內部,變化的僅僅是需要promise化的api。其實內部實現的細節也是很簡單可以看懂的,就是將promise化后的參數取出來,再多加一個node傳統形式的回調,一同apply進api中。我們通過簡單的wrapPromise直接得到一個promise化的異步api。
總結
到此,promise三部曲,總算是講完了。在我總結寫blog時,也是做了比較多的思考,有些地方也可能表意不清。我們知道其實promise,其實是另一種形式的回調,只是它的形式我們更喜歡,也更自然。我們唯一會煩惱的是,我們需要為不同場景的異步api進行Promise化。但是為了更好的控制,我認為也是值得嘗試的。promise單獨使用,并不能體現它強大的地方。因為接下來我們會講promise和Generator配合,展現強大的異步編程能力。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
