前言
node的核心編程思想是異步編程,ebookcoin是基於node開發的,ebookcoin的異步編程是基於流程控制庫async實現的(當然,不僅僅限於async這個庫,還有其他的方法,因爲是一篇淺析文章,因此,本文不進行分析、討論)。本文將會圍繞着如何利用async完成異步編程開發進行講解,還會對ebookcoin中出現的其他異步編程方式進行膚淺的分析和介紹。
async流程控制庫
async是一個流程控制庫,官網網址:http://caolan.github.io/async/。截止到發稿前,async的版本是2.1.4。async提供了三類主要方法,分別是Collections(控制集合的流程控制方法,如何理解呢,簡單的講就是方法的第一個參數可能是一個集合col,或者是一個對象object,或者是一個數組array,通過後邊的代碼來主要操作這第一個參數中的對象)、Control Flow(流程控制方法,標準的流程控制方法)、Utils(流程控制的公共方法)。這三類方法相互協作,共同構建起了async的流程控制庫。
ebookcoin中出現的async流程控制方法
目前,async的流程控制方法一共有72個,ebookcoin中用到了10個,如下:
Collections方法 | 說明 |
---|---|
eachSeries | 別名forEachSeries,格式-eachSeries(coll, iteratee, callbackopt) , 該方法和each類似,不過,同一時間只運行一個async的操作,我們將在下方看到例子和講解。 |
eachLimit | 別名eachLimit,格式-eachLimit(coll, limit, iteratee, callbackopt) , 該方法和each類似,不過,同一時間只運行最大數量不超過limit個的async操作,我們將在下方看到例子和講解。 |
forEachOfSeries | 別名eachOfSeries,格式-eachOfSeries(coll, iteratee, callbackopt) , 該方法和eachOf類似,不過,同一時間只運行一個async的操作,我們將在下方看到例子和講解。 |
Control Flow方法 | 說明 |
---|---|
auto(自動) | 格式-auto(tasks, concurrencyopt, callbackopt) -由庫根據task間的依賴,決定最好的順序來執行tasks,tasks是一個object,裏邊包含了多個子任務和回調函數,我們將在下方看到例子和講解 |
parallel(並行) | 格式-parallel(tasks, callbackopt) ,我們將在下方看到例子和講解。 |
series(串行) | 格式-series(tasks, callbackopt) ,我們將在下方看到例子和講解。 |
whilst(同時) | 格式-whilst(test, iteratee, callbackopt) ,我們將在下方看到例子和講解。 |
until(直到) | 格式-until(test, fn, callbackopt) ,與whilst相反的一個方法,我們將在下方看到例子和講解。 |
retry(重試) | 格式-retry(optsopt, task, callbackopt) ,我們將在下方看到例子和講解。 |
doWhilst | 格式-doWhilst(iteratee, test, callbackopt) ,whilst的 do - while形式,我們將在下方看到例子和講解。 |
each
eachSeries、eachLimit、eachOfSeries(forEachOfSeries)這三個方法都源於each,因此,先講解一下each。
each的別名是forEach,格式each(coll, iteratee, callbackopt)
,其中coll是集合、對象或者數組,iteratee是業務函數,重複操作的函數,用來定義一個重複執行的函數,callbackopt是回調函數。
each的功能是並行的將iteratee函數應用於coll的每個元素,並在結束時調用每個元素自己的回調函數(callback()),如果iteratee向自己的callback傳遞了一個error,則外層each方法的callback(可以認爲是外層的那個callback)將馬上接收到這個error。需要注意的是,由於程序是並行運行的,因此,不能保證函數的執行完全遵照順序進行。
接下來,我們來看一下例子:
var async = require('async');
var openFiles = ["package.json","markdown.js","abc.js"];
async.each(openFiles, function(file, callback) {
console.log('Processing file ' + file);
if( file.length > 32 ) {
console.log('This file name is too long');
callback('File name too long');
} else {
console.log('File processed');
callback();
}
}, function(err) {
if( err ) {
console.log('A file failed to process');
} else {
console.log('All files have been processed successfully');
}
});
輸出的結果是:
我們可以看到結果是順序執行的,依次是”package.json”,”markdown.js”,”abc.js”,但是,官方文檔給的說明中明確表示,不能100%的保證執行順序,恩,因爲是並行的,此處要注意。
eachSeries
該方法的使用和each一樣,不過,同一時間只運行一個async的操作,但是在實驗的過程中我發現還是有很大區別的,我們來看兩個例子:
//我對原來的函數進行了修改
var async = require('async');
var openFiles = ["package.json","markdown.js","abc.js","abc2.js"];
async.each(openFiles, function(file, callback) {
console.log('Processing file ' + file);
if( file.length > 32 ) {
console.log('This file name is too long');
callback('File name too long');
} else if(file==="markdown.js"){
console.log('File processing');
setTimeout(function(){console.log('0,File processed')},0);
}else{
console.log('File processed');
callback();
}
}, function(err) {
if( err ) {
console.log('A file failed to process');
} else {
console.log('All files have been processed successfully');
}
});
結果如下:
//改爲eachSeries
var async = require('async');
var openFiles = ["package.json","markdown.js","abc.js","abc2.js"];
async.eachSeries(openFiles, function(file, callback) {
console.log('Processing file ' + file);
if( file.length > 32 ) {
console.log('This file name is too long');
callback('File name too long');
} else if(file==="markdown.js"){
console.log('File processing');
setTimeout(function(){console.log('0,File processed')},0);
}else{
console.log('File processed');
callback();
}
}, function(err) {
if( err ) {
console.log('A file failed to process');
} else {
console.log('All files have been processed successfully');
}
});
我們看到區別了吧,使用eachSeries時,如果出現了中斷的函數,則程序會執行中斷函數中的內容,然後不再向後執行了。但是,這個是現象,原因我就不懂了。找時間,找人討論一下…..
eachLimit
該方法和each的使用類似,不過,同一時間只運行最大數量不超過limit個的async操作。不過當limit=1的時候,eachLimit 和eachSeries是沒有區別的,當limit!=1的時候,eachLimit會更像each。當然,limit=0的時候,這段程序會直接執行callback裏的程序,console.log(‘All files have been processed successfully’);
我們先看limit=0的時候的程序效果:
var async = require('async');
var openFiles = ["package.json","markdown.js","abc.js","abc2.js"];
async.eachLimit(openFiles, 0,function(file, callback) {
console.log('Processing file ' + file);
if( file.length > 32 ) {
console.log('This file name is too long');
callback('File name too long');
} else if(file==="markdown.js"){
console.log('File processing');
setTimeout(function(){console.log('0,File processed')},0);
}else{
console.log('File processed');
callback();
}
}, function(err) {
if( err ) {
console.log('A file failed to process');
} else {
console.log('All files have been processed successfully');
}
});
效果如下:
當limit=1的時候的效果。
var async = require('async');
var openFiles = ["package.json","markdown.js","abc.js","abc2.js"];
async.eachLimit(openFiles, 1,function(file, callback) {
console.log('Processing file ' + file);
if( file.length > 32 ) {
console.log('This file name is too long');
callback('File name too long');
} else if(file==="markdown.js"){
console.log('File processing');
setTimeout(function(){console.log('0,File processed')},0);
}else{
console.log('File processed');
callback();
}
}, function(err) {
if( err ) {
console.log('A file failed to process');
} else {
console.log('All files have been processed successfully');
}
});
limit=1的時候,與eachSeries相似,我們看結果
limit>1的時候的效果
var async = require('async');
var openFiles = ["package.json","markdown.js","abc.js","abc2.js"];
async.eachLimit(openFiles, 2,function(file, callback) {
console.log('Processing file ' + file);
if( file.length > 32 ) {
console.log('This file name is too long');
callback('File name too long');
} else if(file==="markdown.js"){
console.log('File processing');
setTimeout(function(){console.log('0,File processed')},0);
}else{
console.log('File processed');
callback();
}
}, function(err) {
if( err ) {
console.log('A file failed to process');
} else {
console.log('All files have been processed successfully');
}
});
因此通過上邊的例子我們可以看出,each、eachSeries、eachLimit的區別就是同一時間內有多少個async在操作,each是默認並行的,eachSeries只有一個,eachLimit可以控制async的數量,當然,limit只是理論上的最大值,不一定會有limit個。
eachOf、eachOfSeries
我的程序跑不了這個函數,之後補充,用法就是比each多了一個key作爲iteratee的第二個參數。
var obj = {dev: "/dev.json", test: "/test.json", prod: "/prod.json"};
var configs = {};
async.forEachOf(obj, function (value, key, callback) {
fs.readFile(__dirname + value, "utf8", function (err, data) {
if (err) return callback(err);
try {
configs[key] = JSON.parse(data);
} catch (e) {
return callback(e);
}
callback();
});
}, function (err) {
if (err) console.error(err.message);
// configs is now a map of JSON data
doSomethingWith(configs);
});
這個key是遍歷的遊標,通過key可以記錄循環到了list的哪個對象了。
auto的例子
該方法的格式爲auto(tasks, concurrencyopt, callbackopt)
auto這個方法有幾個特性
1.auto會決定tasks中的函數和依賴的一個最好的執行順序,每個函數都可以選擇是否需要完成前置任務。
2.如果一個函數產生錯誤,則整個流程都會停止,其他的任務將不會被執行,auto的callback將會馬上收到這個錯誤,並結束整個auto流程。
3.當前置任務,完成之後,會將結果傳遞到依賴他的函數中,如果沒有依賴,則只會調用該任務的callback
在ebookcoin中,app.js加載服務器的方式就是利用了auto這個方法。好了,我們來看一個例子
async.auto({
// this function will just be passed a callback
readData: async.apply(fs.readFile, 'data.txt', 'utf-8'),
showData: ['readData', function(results, cb) {
// results.readData is the file's contents
// ...
}]
}, callback);
async.auto({
get_data: function(callback) {
console.log('in get_data');
// async code to get some data
callback(null, 'data', 'converted to array');
},
make_folder: function(callback) {
console.log('in make_folder');
// async code to create a directory to store a file in
// this is run at the same time as getting the data
callback(null, 'folder');
},
write_file: ['get_data', 'make_folder', function(results, callback) {
console.log('in write_file', JSON.stringify(results));
// once there is some data and the directory exists,
// write the data to a file in the directory
callback(null, 'filename');
}],
email_link: ['write_file', function(results, callback) {
console.log('in email_link', JSON.stringify(results));
// once the file is written let's email a link to it...
// results.write_file contains the filename returned by write_file.
callback(null, {'file':results.write_file, 'email':'[email protected]'});
}]
}, function(err, results) {
console.log('err = ', err);
console.log('results = ', results);
});
parallel的例子
該方法的格式爲:parallel(tasks, callbackopt)
該方法的特性如下:
1.並行運行tasks中的函數,並將執行結構以數組的形式保存到callback中
2.任意一個函數報錯,都會向parallel的callback傳遞一個error。(官網沒說會不會停止整個方法)
3.parallel是一個kicking-off I/O,而不是並行執行的代碼,也就是說,如果沒有人爲添加的並行操作,則該功能就是一個順序執行的函數,如果存在異步執行的函數,纔會並行的去執行。(很拗口吧,一會看例子)
async.parallel([
function(callback) {
setTimeout(function() {
callback(null, 'one');
}, 200);
},
function(callback) {
setTimeout(function() {
callback(null, 'two');
}, 100);
}
],
// optional callback
function(err, results) {
// the results array will equal ['one','two'] even though
// the second function had a shorter timeout.
console.log('results = ', results);
});
// an example using an object instead of an array
async.parallel({
one: function(callback) {
setTimeout(function() {
callback(null, 1);
}, 200);
},
two: function(callback) {
setTimeout(function() {
callback(null, 2);
}, 100);
}
}, function(err, results) {
console.log('results = ', results);
});
series
格式:series(tasks, callbackopt)
特性:
1.順序執行tasks中的函數,tasks可以是對象(返回對象到callback)或者數組(返回數組到callback)
2.一個函數報錯則方法停止,傳遞一個error到callback
3.因爲ES5的特殊性,如果想在各個平臺上都是按照自己定義的順序去執行函數的話,最好還是用array in tasks
我們來看一個例子:
async.series([
function(callback) {
// do some stuff ...
callback(null, 'one');
},
function(callback) {
// do some more stuff ...
callback(null, 'two');
}
],
// optional callback
function(err, results) {
// results is now equal to ['one', 'two']
console.log('results = ', results);
});
async.series({
one: function(callback) {
setTimeout(function() {
callback(null, 1);
}, 200);
},
two: function(callback){
setTimeout(function() {
callback(null, 2);
}, 100);
}
}, function(err, results) {
// results is now equal to: {one: 1, two: 2}
console.log('results = ', results);
});
從例子中,我們看到使用series的好處了吧,可以最大限度的將異步的程序寫成同步的樣式。
whilst、doWhilst
格式:whilst(test, iteratee, callbackopt)
特性:只要test返回true,就重複調用iteratee,有錯也停止,看例子:
var count = 0;
async.whilst(
function() { return count < 5; },
function(callback) {
count++;
setTimeout(function() {
callback(null, count);
}, 30);
},
function (err, n) {
// 5 seconds have passed, n = 5
console.log(n);
}
);
這個例子我這個程序一直返回undefined,不知道什麼原因,因此,不截圖了
doWhilst的格式:doWhilst(iteratee, test, callbackopt)
doWhilst和whilst的關係,跟do while 和 while的關係一樣,都是先執行一次,另外一個不同的地方是,參數的位置不一樣,呵呵呵
until
格式:until(test, fn, callbackopt)
特性:
1.不斷的執行fn,直到test 返回true。
2.方法存在錯誤將會停止,並向fn的callback傳遞error。
用法與whilst相反。
retry
格式爲:retry(optsopt, task, callbackopt)
特性:
1.嘗試從task中得到正確的響應
2.有錯則會嘗試最後一次函數調用
我們來看格式:
// The `retry` function can be used as a stand-alone control flow by passing
// a callback, as shown below:
// try calling apiMethod 3 times
async.retry(3, apiMethod, function(err, result) {
// do something with the result
});
// try calling apiMethod 3 times, waiting 200 ms between each retry
async.retry({times: 3, interval: 200}, apiMethod, function(err, result) {
// do something with the result
});
// try calling apiMethod 10 times with exponential backoff
// (i.e. intervals of 100, 200, 400, 800, 1600, ... milliseconds)
async.retry({
times: 10,
interval: function(retryCount) {
return 50 * Math.pow(2, retryCount);
}
}, apiMethod, function(err, result) {
// do something with the result
});
// try calling apiMethod the default 5 times no delay between each retry
async.retry(apiMethod, function(err, result) {
// do something with the result
});
// try calling apiMethod only when error condition satisfies, all other
// errors will abort the retry control flow and return to final callback
async.retry({
errorFilter: function(err) {
return err.message === 'Temporary error'; // only retry on a specific error
}
}, apiMethod, function(err, result) {
// do something with the result
});
// It can also be embedded within other control flow functions to retry
// individual methods that are not as reliable, like this:
async.auto({
users: api.getUsers.bind(api),
payments: async.retry(3, api.getPayments.bind(api))
}, function(err, results) {
// do something with the results
});
我們再來看ebookcoin中的例子:
async.retry(20, function (cb) {
modules.peer.list(config, function (err, peers) {
if (!err && peers.length) {
var peer = peers[0];
self.getFromPeer(peer, options, cb);
} else {
return cb(err || "No peers in db");
}
});
}, function (err, results) {
cb(err, results);
});
ebookcoin程序的意思是,在不報錯的前提下,最多將這個匿名函數執行20次。self.getFromPeer(peer, options, cb);這個功能是尋找網絡中的peer節點。
總結
本文簡單介紹了async庫以及ebookcoin用了async的哪些方法,並對這些方法從流程控制的角度進行了淺析。node的異步編程博大精深,ebookcoin的源碼也精彩絕倫(crypti的源碼就寫的很好了,通過對ebookcoin和crypti的源碼,給了我在程序設計和實現上很多的啓發,真好,能站在巨人們的肩膀上,真好),本文僅僅算是拋磚引玉的文章,希望通過該文激起大家學習node,使用node的興起,同時,也需要大家多提意見,我們共同成長,歡迎越來越多的小夥伴們進入到node領域,進入到區塊鏈研究的領域。
後記
下一篇文章計劃解讀一下ebookcoin的編解碼處的代碼,當然都是淺析,都是先從最基本的技術實現入手進行解讀,我希望自己能夠堅持下來,爭取一週能寫一篇文章。
歡迎大家添加好友,作者本人的微信:(作者:Ryan)
歡迎大家添加關注,作者本人的微信公衆號:(作者:Ryan)