淺析Node.js中的流程控制
對于在node這種異步框架下的編程,唯一的難題是:如何控制哪些函數(shù)順序執(zhí)行,哪些函數(shù)并行執(zhí)行。node中并沒有內置的控制方法,在這里我分享編寫本站程序時用到的一些技巧。
并行VS順序
在應用程序中通常有一些步驟必須在先前的操作得出結果之后才能運行。在平常的順序執(zhí)行的程序中這非常容易解決,因為每一部分都必須等待前一部分執(zhí)行完畢才能執(zhí)行。
Node中,除了那些執(zhí)行阻塞IO的方法,其他方法都會存在這個問題。比如,掃描文件夾、打開文件、讀取文件內容、查詢數(shù)據(jù)庫等等。
對于我的博客引擎,有一些以樹形結構組織的文件需要處理。步驟如下所示:
◆ 獲取文章列表 (譯者注:作者的博客采取文件系統(tǒng)存儲文章,獲取文章列表,起始就是掃描文件夾)。
◆ 讀入并解析文章數(shù)據(jù)。
◆ 獲取作者列表。
◆ 讀取并解析作者數(shù)據(jù)。
◆ 獲取HAML模版列表。
◆ 讀取所有HAML模版。
◆ 獲取資源文件列表。
◆ 讀取所有資源文件。
◆ 生成文章html頁面。
◆ 生成作者頁面。
◆ 生成索引頁(index page)。
◆ 生成feed頁。
◆ 生成靜態(tài)資源文件。
如你所見,有些步驟可以不依賴其他步驟獨立執(zhí)行(但有些不行)。例如,我可以同時讀取所有文件,但必須在掃描文件夾獲取文件列表之后。我可以同時寫入所有文件,但是必須等待文件內容都計算完畢才能寫入。
使用分組計數(shù)器
對于如下這個掃貓文件夾并讀取其中文件的例子,我們可以使用一個簡單的計數(shù)器:
- var fs = require('fs');
- fs.readdir(".", function (err, files) {
- var count = files.length,
- results = {};
- files.forEach(function (filename) {
- fs.readFile(filename, function (data) {
- results[filename] = data;
- count--;
- if (count <= 0) {
- // Do something once we know all the files are read.
- }
- });
- });
- });
嵌套回調函數(shù)是保證它們順序執(zhí)行的好方法。所以在readdir回調函數(shù)中,我們根據(jù)文件數(shù)量設定了一個倒數(shù)計數(shù)器。然后我們對每個文件執(zhí)行readfile操作,它們將并行執(zhí)行并以任意順序完成。最重要的是,在每個文件讀取完成時計數(shù)器的值會減小1,當它的值變?yōu)?的時候我們就知道文件全部讀取完畢了。
通過傳遞回調函數(shù)避免過度嵌套
在取得文件內容之后,我們可以在最里層的函數(shù)中執(zhí)行其他操作。但是當順序操作超過7級之后,這將很快成為一個問題。
讓我們使用傳遞回調的方式修改一下上面的實例:
- var fs = require('fs');
- function read_directory(path, next) {
- fs.readdir(".", function (err, files) {
- var count = files.length,
- results = {};
- files.forEach(function (filename) {
- fs.readFile(filename, function (data) {
- results[filename] = data;
- count--;
- if (count <= 0) {
- next(results);
- }
- });
- });
- });
- }
- function read_directories(paths, next) {
- var count = paths.length,
- data = {};
- paths.forEach(function (path) {
- read_directory(path, function (results) {
- data[path] = results;
- count--;
- if (count <= 0) {
- next(data);
- }
- });
- });
- }
- read_directories(['articles', 'authors', 'skin'], function (data) {
- // Do something
- });
現(xiàn)在我們寫了一個混合的異步函數(shù),它接收一些參數(shù)(本例中為路徑),和一個在完成所有操作后調用的回調函數(shù)。所有的操作都將在回調函數(shù)中完成,最重要的是我們將多層嵌套轉化為一個非嵌套的回調函數(shù)。
Combo庫
我利用空閑時間編寫了一個簡單的Combo庫?;旧?,它封裝了進行事件計數(shù),并在所有事件完成之后調用回調函數(shù)的這個過程。同時它也保證不管回調函數(shù)的實際執(zhí)行時間,都能保證它們按照注冊的順序執(zhí)行。
- function Combo(callback) {
- this.callback = callback;
- this.items = 0;
- this.results = [];
- }
- Combo.prototype = {
- add: function () {
- var self = this,
- id = this.items;
- this.items++;
- return function () {
- self.check(id, arguments);
- };
- },
- check: function (id, arguments) {
- this.results[id] = Array.prototype.slice.call(arguments);
- this.items--;
- if (this.items == 0) {
- this.callback.apply(this, this.results);
- }
- }
- };
如果你想從數(shù)據(jù)庫和文件中讀取數(shù)據(jù),并在完成之后執(zhí)行一些操作,你可以如下進行:
- // Make a Combo object.
- var both = new Combo(function (db_result, file_contents) {
- // Do something
- });
- // Fire off the database query
- people.find({name: "Tim", age: 27}, both.add());
- // Fire off the file read
- fs.readFile('famous_quotes.txt', both.add());
數(shù)據(jù)庫查詢和文件讀取將同時開始,當他們全部完成之后,傳遞給combo構造函數(shù)的回調函數(shù)將會被調用。第一個參數(shù)是數(shù)據(jù)庫查詢結果,第二個參數(shù)是文件讀取結果。
結論
本篇文章中介紹的技巧:
◆ 通過嵌套回調,得到順序執(zhí)行的行為。
◆ 通過直接函數(shù)調用,得到并行執(zhí)行的行為。
◆ 通過回調函數(shù)來化解順序操作造成的嵌套。
◆ 使用計數(shù)器檢測一組并行的操作什么時候完成。
◆ 使用類似combo這樣的庫來簡化操作。
#p#
上一篇介紹流程控制的文章給我?guī)砹撕艽蟮臉啡?,現(xiàn)在我想要處理一些反饋,另外還要討論一下inimino所作的偉大工作。
當前node中有兩種處理異步返回值的方法:promises和event emitters。關于兩種方法的細節(jié),你可以閱讀nodejs.org上的介紹。我將會討論這兩種方法和另一種處理異步返回值和流事件(streaming events)的方法。
為什么要區(qū)分Promise和EventEmitter?
在node中有兩種處理事件的類,它們是:Promise和EventEmitter。Promise是函數(shù)的異步表現(xiàn)形式。
- var File = require('file');
- var promise = File.read('mydata.txt');
- promise.addCallback(function (text) {
- // Do something
- });
- promise.addErrback(function (err) {
- // Handle error
- })
File.read接受文件名并返回文件內容。
有時我們需要監(jiān)聽可能多次發(fā)生的事件。例如,在一個web服務中,處理web請求時,body事件多次被觸發(fā),然后complete事件被觸發(fā)。
- Http.createServer(function (req, res) {
- var body = "";
- req.addListener('body', function (chunk) {
- body += chunk;
- });
- req.addListener('complete', function () {
- // Do something with body text
- });
- }).listen(8080);
這兩種方式的不同之處在于:在使用promise時,你會得到success事件或者error事件,但不會同時得到,也不會得到一個以上事件。在處理會發(fā)生多次的事件的時候,你就需要更強大的 EventEmitters。
創(chuàng)建自定義promise
假定我想為posix.open, posix.write, 和posix.close寫一個便于使用的包裝函數(shù)filewrite。(如下代碼摘自”file”函數(shù)庫中File.write函數(shù)的真實代碼)
- function fileWrite (filename, data) {
- var promise = new events.Promise();
- posix.open(filename, "w", 0666)
- .addCallback(function (fd) {
- function doWrite (_data) {
- posix.write(fd, _data, 0, encoding)
- .addCallback(function (written) {
- if (written === _data.length) {
- posix.close(fd);
- promise.emitSuccess();
- } else {
- doWrite(_data.slice(written));
- }
- }).addErrback(function () {
- promise.emitError();
- });
- }
- doWrite(data);
- })
- .addErrback(function () {
- promise.emitError();
- });
- return promise;
- };
filewrite函數(shù)可以以如下形式使用:
- fileWrite("MyBlog.txt", "Hello World").addCallback(function () {
- // It's done
- });
請注意,我必須創(chuàng)建一個promise對象,執(zhí)行操作,然后將結果傳遞給這個promise對象。
還有更好的方法
promises工作良好,但是繼續(xù)讀過inimino之后,它所使用的方法令我印象深刻。
是否還記得我們的第一個例子?假設我們按照如下方式使用File.read:
- var File = require('file');
- File.read('mydata.txt')(function (text) {
- // Do something
- }, function (error) {
- // Handle error
- });
它不返回promise對象,而是返回一個接受兩個回調函數(shù)作為參數(shù)的函數(shù):一個處理成功,一個處理失敗。我把這種風格成為“Do風格”,下面我詳細解釋:
編寫回調風格的代碼
如果我們想定義一個不立刻返回值的函數(shù)。使用”Do”風格,filewirte函數(shù)應當如下使用:(假定之前提到的posix函數(shù)也是這個風格)
- function fileWrite (filename, data) { return function (callback, errback) {
- posix.open(filename, "w", 0666)(function (fd) {
- function doWrite (_data) {
- posix.write(fd, _data, 0, encoding)(
- function (written) {
- if (written === _data.length) {
- posix.close(fd);
- callback();
- } else {
- doWrite(_data.slice(written));
- }
- }, errback);
- }
- doWrite(data);
- }, errback);
- }};
請注意,這樣很容易就把錯誤信息返回給了調用者。同時,這種風格也使代碼更短,更易閱讀。
使用這種風格編寫代碼的關鍵是:不要返回promise,而是返回一個接受兩個回調的函數(shù),在需要的時候直接調用返回的函數(shù)。
“Do”函數(shù)庫
前些日子我寫了一個小型的函數(shù)庫,叫做“Do”。實際上,它只有一個執(zhí)行并行操作的函數(shù),就像上一篇文章中介紹的“Combo”庫。
實現(xiàn)
如下是整個函數(shù)的實現(xiàn):
- Do = {
- parallel: function (fns) {
- var results = [],
- counter = fns.length;
- return function(callback, errback) {
- fns.forEach(function (fn, i) {
- fn(function (result) {
- results[i] = result;
- counter--;
- if (counter <= 0) {
- callback.apply(null, results);
- }
- }, errback);
- });
- }
- }
- };
結合回調風格,使用這個函數(shù)可以寫出非常強大和簡介的代碼。
執(zhí)行單個操作
我們假定有一個實現(xiàn)了這個新技巧的函數(shù)readFIle,可以如下使用這個函數(shù):
- // A single async action with error handling
- readFile('secretplans.txt')(function (secrets) {
- // Do something
- }, function (error) {
- // Handle Error
- });
執(zhí)行并行操作
我們繼續(xù)使用”Do"函數(shù)庫
- Do.parallel([
- readFile('mylib.js'),
- readFile('secretplans.txt'),
- ])(function (source, secrets) {
- // Do something
- }, function (error) {
- // Handle Error
- });
上述代碼代碼并行執(zhí)行了兩個異步操作,并在全部執(zhí)行完畢后執(zhí)行指定代碼。注意,如果沒有錯誤發(fā)生,只有處理success的回調函數(shù)會被執(zhí)行。如果出錯,函數(shù)會將錯誤傳遞給通常的錯誤處理代碼。
你也可傳遞一個文件名數(shù)組。
- var files = ["one.txt", "two.txt", "three.txt"];
- var actions = files.map(function (filename) {
- return readFile(filename);
- });
- Do.parallel(actions)(function () {
- var contents = {},
- args = arguments;
- files.forEach(function (filename, index) {
- contents[filename] = args[index];
- });
- // Do something
- });
- // Let error thow exception.
執(zhí)行順序操作
要執(zhí)行順序操作,只需將函數(shù)“串起來”即可:
- readFile('names.txt')(
- function upcase_slowly(string) { return function (next) {
- setTimeout(function () {
- next(string.toUpperCase());
- }, 100);
- }}
- )(
- function save_data(string) { return function (next) {
- writeFile('names_up.txt', string)(next);
- }}
- )(function () {
- // File was saved
- });
上述代碼讀取文件'names.txt',完成之后調用upcase_slowly,然后將生成的新字符串專遞給save_data函數(shù)。save_data函數(shù)是對writeFile的一個包裝。當save_data函數(shù)執(zhí)行完畢之后,將執(zhí)行最終的回調函數(shù)。
Just for fun, here is the same example translated to the Jack language (still in development).
開個玩笑,如下代碼是翻譯成Jack語言(還在開發(fā)中)的示例代碼:
- readFile names.txt
- | fun string -> next ->
- timeout 100, fun ->
- next string.toUpperCase()
- | fun string -> next ->
- writeFile 'names_up.txt', string | next
- | fun ->
- # File was saved
原文:http://www.grati.org/?cat=35
【編輯推薦】