新聞中心
對(duì)于在node這種異步框架下的編程,唯一的難題是:如何控制哪些函數(shù)順序執(zhí)行,哪些函數(shù)并行執(zhí)行。node中并沒有內(nèi)置的控制方法,在這里我分享編寫本站程序時(shí)用到的一些技巧。

并行VS順序
在應(yīng)用程序中通常有一些步驟必須在先前的操作得出結(jié)果之后才能運(yùn)行。在平常的順序執(zhí)行的程序中這非常容易解決,因?yàn)槊恳徊糠侄急仨毜却耙徊糠謭?zhí)行完畢才能執(zhí)行。
Node中,除了那些執(zhí)行阻塞IO的方法,其他方法都會(huì)存在這個(gè)問題。比如,掃描文件夾、打開文件、讀取文件內(nèi)容、查詢數(shù)據(jù)庫等等。
對(duì)于我的博客引擎,有一些以樹形結(jié)構(gòu)組織的文件需要處理。步驟如下所示:
◆ 獲取文章列表 (譯者注:作者的博客采取文件系統(tǒng)存儲(chǔ)文章,獲取文章列表,起始就是掃描文件夾)。
◆ 讀入并解析文章數(shù)據(jù)。
◆ 獲取作者列表。
◆ 讀取并解析作者數(shù)據(jù)。
◆ 獲取HAML模版列表。
◆ 讀取所有HAML模版。
◆ 獲取資源文件列表。
◆ 讀取所有資源文件。
◆ 生成文章html頁面。
◆ 生成作者頁面。
◆ 生成索引頁(index page)。
◆ 生成feed頁。
◆ 生成靜態(tài)資源文件。
如你所見,有些步驟可以不依賴其他步驟獨(dú)立執(zhí)行(但有些不行)。例如,我可以同時(shí)讀取所有文件,但必須在掃描文件夾獲取文件列表之后。我可以同時(shí)寫入所有文件,但是必須等待文件內(nèi)容都計(jì)算完畢才能寫入。
使用分組計(jì)數(shù)器
對(duì)于如下這個(gè)掃貓文件夾并讀取其中文件的例子,我們可以使用一個(gè)簡(jiǎn)單的計(jì)數(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.
- }
- });
- });
- });
嵌套回調(diào)函數(shù)是保證它們順序執(zhí)行的好方法。所以在readdir回調(diào)函數(shù)中,我們根據(jù)文件數(shù)量設(shè)定了一個(gè)倒數(shù)計(jì)數(shù)器。然后我們對(duì)每個(gè)文件執(zhí)行readfile操作,它們將并行執(zhí)行并以任意順序完成。最重要的是,在每個(gè)文件讀取完成時(shí)計(jì)數(shù)器的值會(huì)減小1,當(dāng)它的值變?yōu)?的時(shí)候我們就知道文件全部讀取完畢了。
通過傳遞回調(diào)函數(shù)避免過度嵌套
在取得文件內(nèi)容之后,我們可以在最里層的函數(shù)中執(zhí)行其他操作。但是當(dāng)順序操作超過7級(jí)之后,這將很快成為一個(gè)問題。
讓我們使用傳遞回調(diào)的方式修改一下上面的實(shí)例:
- 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)在我們寫了一個(gè)混合的異步函數(shù),它接收一些參數(shù)(本例中為路徑),和一個(gè)在完成所有操作后調(diào)用的回調(diào)函數(shù)。所有的操作都將在回調(diào)函數(shù)中完成,最重要的是我們將多層嵌套轉(zhuǎn)化為一個(gè)非嵌套的回調(diào)函數(shù)。
Combo庫
我利用空閑時(shí)間編寫了一個(gè)簡(jiǎn)單的Combo庫?;旧?,它封裝了進(jìn)行事件計(jì)數(shù),并在所有事件完成之后調(diào)用回調(diào)函數(shù)的這個(gè)過程。同時(shí)它也保證不管回調(diào)函數(shù)的實(shí)際執(zhí)行時(shí)間,都能保證它們按照注冊(cè)的順序執(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í)行一些操作,你可以如下進(jìn)行:
- // 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ù)庫查詢和文件讀取將同時(shí)開始,當(dāng)他們?nèi)客瓿芍?,傳遞給combo構(gòu)造函數(shù)的回調(diào)函數(shù)將會(huì)被調(diào)用。第一個(gè)參數(shù)是數(shù)據(jù)庫查詢結(jié)果,第二個(gè)參數(shù)是文件讀取結(jié)果。
結(jié)論
本篇文章中介紹的技巧:
◆ 通過嵌套回調(diào),得到順序執(zhí)行的行為。
◆ 通過直接函數(shù)調(diào)用,得到并行執(zhí)行的行為。
◆ 通過回調(diào)函數(shù)來化解順序操作造成的嵌套。
◆ 使用計(jì)數(shù)器檢測(cè)一組并行的操作什么時(shí)候完成。
◆ 使用類似combo這樣的庫來簡(jiǎn)化操作。
#p#
上一篇介紹流程控制的文章給我?guī)砹撕艽蟮臉啡ぃF(xiàn)在我想要處理一些反饋,另外還要討論一下inimino所作的偉大工作。
當(dāng)前node中有兩種處理異步返回值的方法:promises和event emitters。關(guān)于兩種方法的細(xì)節(jié),你可以閱讀nodejs.org上的介紹。我將會(huì)討論這兩種方法和另一種處理異步返回值和流事件(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接受文件名并返回文件內(nèi)容。
有時(shí)我們需要監(jiān)聽可能多次發(fā)生的事件。例如,在一個(gè)web服務(wù)中,處理web請(qǐng)求時(shí),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時(shí),你會(huì)得到success事件或者error事件,但不會(huì)同時(shí)得到,也不會(huì)得到一個(gè)以上事件。在處理會(huì)發(fā)生多次的事件的時(shí)候,你就需要更強(qiáng)大的 EventEmitters。
創(chuàng)建自定義promise
假定我想為posix.open, posix.write, 和posix.close寫一個(gè)便于使用的包裝函數(shù)filewrite。(如下代碼摘自”file”函數(shù)庫中File.write函數(shù)的真實(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
- });
請(qǐng)注意,我必須創(chuàng)建一個(gè)promise對(duì)象,執(zhí)行操作,然后將結(jié)果傳遞給這個(gè)promise對(duì)象。
還有更好的方法
promises工作良好,但是繼續(xù)讀過inimino之后,它所使用的方法令我印象深刻。
是否還記得我們的第一個(gè)例子?假設(shè)我們按照如下方式使用File.read:
- var File = require('file');
- File.read('mydata.txt')(function (text) {
- // Do something
- }, function (error) {
- // Handle error
- });
它不返回promise對(duì)象,而是返回一個(gè)接受兩個(gè)回調(diào)函數(shù)作為參數(shù)的函數(shù):一個(gè)處理成功,一個(gè)處理失敗。我把這種風(fēng)格成為“Do風(fēng)格”,下面我詳細(xì)解釋:
編寫回調(diào)風(fēng)格的代碼
如果我們想定義一個(gè)不立刻返回值的函數(shù)。使用”Do”風(fēng)格,filewirte函數(shù)應(yīng)當(dāng)如下使用:(假定之前提到的posix函數(shù)也是這個(gè)風(fēng)格)
- 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);
- }};
請(qǐng)注意,這樣很容易就把錯(cuò)誤信息返回給了調(diào)用者。同時(shí),這種風(fēng)格也使代碼更短,更易閱讀。
使用這種風(fēng)格編寫代碼的關(guān)鍵是:不要返回promise,而是返回一個(gè)接受兩個(gè)回調(diào)的函數(shù),在需要的時(shí)候直接調(diào)用返回的函數(shù)。
“Do”函數(shù)庫
前些日子我寫了一個(gè)小型的函數(shù)庫,叫做“Do”。實(shí)際上,它只有一個(gè)執(zhí)行并行操作的函數(shù),就像上一篇文章中介紹的“Combo”庫。
實(shí)現(xiàn)
如下是整個(gè)函數(shù)的實(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);
- });
- }
- }
- };
結(jié)合回調(diào)風(fēng)格,使用這個(gè)函數(shù)可以寫出非常強(qiáng)大和簡(jiǎn)介的代碼。
執(zhí)行單個(gè)操作
我們假定有一個(gè)實(shí)現(xiàn)了這個(gè)新技巧的函數(shù)readFIle,可以如下使用這個(gè)函數(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í)行了兩個(gè)異步操作,并在全部執(zhí)行完畢后執(zhí)行指定代碼。注意,如果沒有錯(cuò)誤發(fā)生,只有處理success的回調(diào)函數(shù)會(huì)被執(zhí)行。如果出錯(cuò),函數(shù)會(huì)將錯(cuò)誤傳遞給通常的錯(cuò)誤處理代碼。
你也可傳遞一個(gè)文件名數(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',完成之后調(diào)用upcase_slowly,然后將生成的新字符串專遞給save_data函數(shù)。save_data函數(shù)是對(duì)writeFile的一個(gè)包裝。當(dāng)save_data函數(shù)執(zhí)行完畢之后,將執(zhí)行最終的回調(diào)函數(shù)。
Just for fun, here is the same example translated to the Jack language (still in development).
開個(gè)玩笑,如下代碼是翻譯成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
【編輯推薦】
- 揭秘Node.js事件
- Node.js初探之hello world
- Node.js初探之與Mysql的交互
- Node.js入門之神秘的服務(wù)器端JavaScript
- 什么是Node.js?
當(dāng)前文章:淺析Node.js中的流程控制
網(wǎng)頁地址:http://m.5511xx.com/article/coejeoc.html


咨詢
建站咨詢
