對於在node這種非同步架構下的編程,唯一的難題是:如何控制哪些函數順序執行,哪些函數並存執行。node中並沒有內建的控制方法,在這裡我分享編寫本站程式時用到的一些技巧。
並行VS順序
在應用程式中通常有一些步驟必須在先前的操作得出結果之後才能運行。在平常的順序執行的程式中這非常容易解決,因為每一部分都必須等待前一部分執行完畢才能執行。
Node中,除了那些執行阻塞IO的方法,其他方法都會存在這個問題。比如,掃描檔案夾、開啟檔案、讀取檔案內容、查詢資料庫等等。
對於我的部落格引擎,有一些以樹形結構組織的檔案需要處理。步驟如下所示:
◆ 擷取文章列表 (譯者註:作者的部落格採取檔案系統儲存文章,擷取文章列表,起始就是掃描檔案夾)。
◆ 讀入並解析文章資料。
◆ 擷取作者列表。
◆ 讀取並解析作者資料。
◆ 擷取HAML模版列表。
◆ 讀取所有HAML模版。
◆ 擷取資源檔列表。
◆ 讀取所有資源檔。
◆ 產生文章html頁面。
◆ 產生作者頁面。
◆ 產生索引頁(index page)。
◆ 產生feed頁。
◆ 產生靜態資源檔案。
如你所見,有些步驟可以不依賴其他步驟獨立執行(但有些不行)。例如,我可以同時讀取所有檔案,但必須在掃描檔案夾擷取檔案清單之後。我可以同時寫入所有檔案,但是必須等待檔案內容都計算完畢才能寫入。
使用分組計數器
對於如下這個掃貓檔案夾並讀取其中檔案的例子,我們可以使用一個簡單的計數器:
- 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.
- }
- });
- });
- });
嵌套回呼函數是保證它們順序執行的好方法。所以在readdir回呼函數中,我們根據檔案數量設定了一個倒數計數器。然後我們對每個檔案執行readfile操作,它們將並存執行並以任意順序完成。最重要的是,在每個檔案讀取完成時計數器的值會減小1,當它的值變為0的時候我們就知道檔案全部讀取完畢了。
通過傳遞迴調函數避免過度嵌套
在取得檔案內容之後,我們可以在最裡層的函數中執行其他動作。但是當順序操作超過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
- });
現在我們寫了一個混合的非同步函數,它接收一些參數(本例中為路徑),和一個在完成所有操作後調用的回呼函數。所有的操作都將在回呼函數中完成,最重要的是我們將多層嵌套轉化為一個非嵌套的回呼函數。
Combo庫
我利用空閑時間編寫了一個簡單的Combo庫。基本上,它封裝了進行事件計數,並在所有事件完成之後調用回呼函數的這個過程。同時它也保證不管回呼函數的實際執行時間,都能保證它們按照註冊的順序執行。
- 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);
- }
- }
- };
如果你想從資料庫和檔案中讀取資料,並在完成之後執行一些操作,你可以如下進行:
- // 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());
資料庫查詢和檔案讀取將同時開始,當他們全部完成之後,傳遞給combo建構函式的回呼函數將會被調用。第一個參數是資料庫查詢結果,第二個參數是檔案讀取結果。
結論
本篇文章中介紹的技巧:
◆ 通過嵌套回調,得到順序執行的行為。
◆ 通過直接函數調用,得到並存執行的行為。
◆ 通過回呼函數來化解順序操作造成的嵌套。
◆ 使用計數器檢測一組並行的操作什麼時候完成。
◆ 使用類似combo這樣的庫來簡化操作。