今天要討論的主要是在 Asynchronous function 和 Loop 之間的關係。用過 Node.js 的朋友們,應該都知道 Node.js 的特色的 Nonblocking I/O 或者另一個說法是 Node.js 的寫法裡面充斥著許多的 callback function。這也是 Node.js 的優點之一,因為這個 Nonblocking 的特色可以讓 Node.js 只有一個 Thread 而遇到多個 I/O 存取的情況下還可以保持存取效能非常的高。
但是這樣的 Nonblocking 的特色會讓大部分的開發者,已經習慣 blocking 寫法的開發者,在開發 Node.js 應用的時候常常會遇到不知所措的錯誤發生。這篇文章就是要來討論其中一個很常見的錯的寫法。錯誤的寫法是在非常常用的 for-loop 上。先把問題述敘一下:
利用 fs 這個 Module 來呈現,某個資料夾裡面所有的子資料夾的名稱。我們會用到 fs 裡的 fs.readdir 和 fs.stat。假設我們產生一個 function 如下
function listDir (rootDir, callBack){當我們在使用這個 listDir 的時候只要給開頭的 dirName 就會在 Console 呈現出,這個 dirName 裡面所有的子資料夾。於是我們準備一下測試用的資料夾。如下圖(藍色的為資料夾)
}
假設我們把上方提到的 fanit_pattern_for.js 這個檔案裡,同一個資料夾(NodePattern)裡有兩個空的子資料夾,各別是 folder1 和 folder2。如此一來寫程式前的準備動作就做好了。
程式方便同樣寫在 anti_pattern_for.js 裡面就要有使用 listDir 的程式碼。如下
listDir(".",function (err, dirs) {
if(!err){
console.log("got folders : "+dirs);
}else{
console.log("error occurs : "+ err);
}
});
使用的時候就把第一個 rootDir 的名稱 "." ,這個 js 檔所存在的目錄當成根目錄,所以我們預期在 Console 的結果就是
got folders : folder1,folder2
工作準備好,我們要來動手寫 listDir 的程式了。首先來看一下很常見的版本。但是是錯的 XD。
var fs = require("fs");
function listDir (dirName, callBack) {
fs.readdir(dirName, function (err, files) {
if(err){
callBack(err);
return;
}
var dirs = [];
for (var i = files.length - 1; i >= 0; i--) {
fs.stat(dirName+"/"+files[i], function (err, stats) {
if(stats.isDirectory()){
dirs.push(files[i]);
}
});
};
callBack(null, dirs);
});
}
上面的程式碼。如果用listDir(".",function (err, dirs) {來呼叫的話,會得到一個出乎我們意料的結果。
if(!err){
console.log("got folders : "+dirs);
}else{
console.log("error occurs : "+ err);
}
});
got folders沒有任何的資料夾?怎麼會是這樣的結果?我們來檢視一下程式碼,看 for-loop 這個地方
for (var i = files.length - 1; i >= 0; i--) {
fs.stat(dirName+"/"+files[i], function (err, stats) {
if(stats.isDirectory()){
dirs.push(files[i]);
}
});
};
看起來的複雜但可以簡單成如下
for (var i = files.length - 1; i >= 0; i--) {
fs.stat(....);
};
就是做 fs.stat 很多次。也沒什麼奇怪,奇怪的是在 fs.stat 本身是 Nonblocking 的機制。整個 for-loop 會認為 fs.stat(); 已經結束,但是 fs.stat(); 裡面的 callback funciton。
fs.stat(dirName+"/"+files[i], function (err, stats) {
if(stats.isDirectory()){
dirs.push(files[i]);
}
});
第二個參數就是 callback function 都還沒被呼叫,for-loop 就結束,然後就呼叫
完整的程式如下。
callBack(null, dirs);
因為整個執行的速度很快 dirs 就沒有值被寫入。就得到空的值。
對於習慣 blocking 寫法的開發者而言,這麼常用的寫法,竟然會在 Nonblocking 裡出錯,對於剛接觸 Node.js 的開發者應該相當的困擾。
那要怎麼解決呢?
讀者介紹一個常用的解決方法,不能用 for-loop 而改用遞迴的方式。如下先來看一種遞回方法來達到和 for-loop 一樣的效果。
基本原型長得像這樣。
function iterator (index) {
if(index < array.length){
asynchronous function () {
// do something
iterator(index+1);
});
}else{
callBack();
}
} ;
iterator(0); // 執行 iterator
更簡單的寫法可以寫成
(function iterator (index) {
if(index < array.length){
asynchronous function () {
// do something
iterator(index+1);
});
}else{
callBack();
}
} )(0);
這樣的寫法,可以想成是 for-loop 的遞迴寫法。我們以一個 array 有三個元素來舉例,可以展開如下。
function iterator (0) {
function iterator (1) {
function iterator (2) {
}
}
}
// 執行下一行程式
這樣的寫法除了可個保證是依照 index 一個一個執行之外,最後一個 index ( 在這邊是 2) 執行完之後,整個程式才可以執行下一行。我們把原本的 for-loop 版本有問題的地方改寫成如下
(function iterator (index) {
if(index < files.length){
fs.stat(rootDir+"/"+files[index], function (err, stats) {
if(stats.isDirectory()){
dirs.push(files[index]);
}
iterator(index+1);
});
}else{
callBack(null, dirs);
}
})(0);
var fs = require("fs");
function listDir (rootDir, callBack) {
fs.readdir(rootDir, function (err, files) {
if(err){
callBack(err);
return;
}
var dirs = [];
(function iterator (index) {
if(index < files.length){
fs.stat(rootDir+"/"+files[index], function (err, stats) {
if (err) {
callBack(err);
return;
};
if(stats.isDirectory()){
dirs.push(files[index]);
}
iterator(index+1);
});
}else{
callBack(null, dirs);
}
})(0);
});
}
listDir(".",function (err, dirs) {
if(!err){
console.log("got folders : "+dirs);
}else{
console.log("error occurs : "+ err);
}
});
就可以看到我們所預期的結果。
got folders : folder1,folder2以上程式碼都放在 GitHub。