2-6 流程控制

(以sync方式使用async函數、避開巢狀callback循序呼叫async callback等奇技淫巧)

建議參考:
  1. http://howtonode.org/control-flow
  2. http://howtonode.org/control-flow-part-ii
  3. http://howtonode.org/control-flow-part-iii
  4. http://blog.mixu.net/2011/02/02/essential-node-js-patterns-and-snippets
這幾篇都是非常經典的NodeJS/Javascript流程控制好文章(阿,mixu是在介紹一些pattern時提到這方面的主題)。不過我還是用幾個簡單的程式介紹一下做法跟概念:


2-6-1 並發與等待

下面的程式參考了mixu文章中的做法:

var wait = function(callbacks, done) {
console.log('wait start');
var counter = callbacks.length;
var results = [];
var next = function(result) {//接收函數執行結果,並判斷是否結束執行
results.push(result);
if(--counter == 0) {
done(results);//如果結束執行,就把所有執行結果傳給指定的callback處理
}
};
for(var i = 0; i < callbacks.length; i++) {//依次呼叫所有要執行的函數
callbacks[i](next);
}
console.log('wait end');
}

wait(
[
function(next){
setTimeout(function(){
console.log('done a');
var result = 500;
next(result)
},500);
},
function(next){
setTimeout(function(){
console.log('done b');
var result = 1000;
next(result)
},1000);
},
function(next){
setTimeout(function(){
console.log('done c');
var result = 1500;
next(1500)
},1500);
}
],
function(results){
var ret = 0, i=0;
for(; i<results.length; i++) {
ret += results[i];
}
console.log('done all. result: '+ret);
}
);

執行結果:
wait start
wait end
done a
done b
done c
done all. result: 3000

可以看出來,其實wait並不是真的等到所有函數執行完才結束執行,而是在所有傳給他的函數執行完畢後(不論同步、非同步),才執行處理結果的函數(也就是done())

不過這樣的寫法,還不夠實用,因為沒辦法實際讓函數可以等待執行完畢,又能當作事件處理函數來實際使用。上面參考到的Tim Caswell的文章,裡面有一種解法,不過還需要額外包裝(在他的例子中)NodeJS核心的fs物件,把一些函數(例如readFile)用Currying處理。類似像這樣:

var fs = require('fs');
var readFile = function(path) {
    return function(callback, errback) {
        fs.readFile(path, function(err, data) {
            if(err) {
                errback();
            } else {
                callback(data);
            }
        });
    };
}

其他部份可以參考Tim Caswell的文章,他的Do.parallel跟上面的wait差不多意思,這裡只提示一下他沒說到的地方。

另外一種做法是去修飾一下callback,當他作為事件處理函數執行後,再用cps的方式取得結果:

<script>
function Wait(fns, done) {
    var count = 0;
    var results = [];
    this.getCallback = function(index) {
        count++;
        return (function(waitback) {
            return function() {
                var i=0,args=[];
                for(;i<arguments.length;i++) {
                    args.push(arguments[i]);
                }
                args.push(waitback);
                fns[index].apply(this, args);
            };
        })(function(result) {
            results.push(result);
            if(--count == 0) {
                done(results);
            }
        });
    }
}
var a = new Wait(
[
function(waitback){
console.log('done a');
var result = 500;
waitback(result)
},
function(waitback){
console.log('done b');
var result = 1000;
waitback(result)
},
function(waitback){
console.log('done c');
var result = 1500;
waitback(result)
}
],
function(results){
var ret = 0, i=0;
for(; i<results.length; i++) {
ret += results[i];
}
console.log('done all. result: '+ret);
}
);
var callbacks = [a.getCallback(0),a.getCallback(1),a.getCallback(0),a.getCallback(2)];
//一次取出要使用的callbacks,避免結果提早送出
setTimeout(callbacks[0], 500);
setTimeout(callbacks[1], 1000);
setTimeout(callbacks[2], 1500);
setTimeout(callbacks[3], 2000);
//當所有取出的callbacks執行完畢,就呼叫done()來處理結果
</script>

執行結果:

done a
done b
done a
done c
done all. result: 3500

上面只是一些小實驗,更成熟的作品是Tim Caswell的step:https://github.com/creationix/step

如果希望真正使用同步的方式寫非同步,則需要使用Promise.js這一類的library來轉換非同步函數,不過他結構比較複雜XD(見仁見智,不過有些人認為Promise有點過頭了):http://blogs.msdn.com/b/rbuckton/archive/2011/08/15/promise-js-2-0-promise-framework-for-javascript.aspx

如果想不透過其他Library做轉換,又能直接用同步方式執行非同步函數,大概就要使用一些需要額外compile原始程式碼的方法了。例如Bruno Jouhier的streamline.js:https://github.com/Sage/streamlinejs


2-6-2 循序執行

循序執行可以協助把非常深的巢狀callback結構攤平,例如用這樣的簡單模組來做(serial.js):

module.exports = function(funs) {
    var c = 0;
    if(!isArrayOfFunctions(funs)) {
        throw('Argument type was not matched. Should be array of functions.');
    }
    return function() {
        var args = Array.prototype.slice.call(arguments, 0);
        if(!(c>=funs.length)) {
            c++;
            return funs[c-1].apply(this, args);
        }
    };
}

function isArrayOfFunctions(f) {
    if(typeof f !== 'object') return false;
    if(!f.length) return false;
    if(!f.concat) return false;
    if(!f.splice) return false;
    var i = 0;
    for(; i<f.length; i++) {
        if(typeof f[i] !== 'function') return false;
    }
    return true;
}

簡單的測試範例(testSerial.js),使用fs模組,確定某個path是檔案,然後讀取印出檔案內容。這樣會用到兩層的callback,所以測試中有使用serial的版本與nested callbacks的版本做對照:

var serial = require('./serial'),
    fs = require('fs'),
    path = './dclient.js',
    cb = serial([
    function(err, data) {
        if(!err) {
            if(data.isFile) {
                fs.readFile(path, cb);
            }
        } else {
            console.log(err);
        }
    },
    function(err, data) {
        if(!err) {
            console.log('[flattened by searial:]');
            console.log(data.toString('utf8'));
        } else {
            console.log(err);
        }
    }
]);
fs.stat(path, cb);

fs.stat(path, function(err, data) {
    //第一層callback
    if(!err) {
        if(data.isFile) {
            fs.readFile(path, function(err, data) {
                //第二層callback
                if(!err) {
                    console.log('[nested callbacks:]');
                    console.log(data.toString('utf8'));
                } else {
                    console.log(err);
                }
            });
        } else {
            console.log(err);
        }
    }
});

關鍵在於,這些callback的執行是有順序性的,所以利用serial返回的一個函數cb來取代這些callback,然後在cb中控制每次會循序呼叫的函數,就可以把巢狀的callback攤平成循序的function陣列(就是傳給serial函數的參數)。

測試中的./dclient.js是一個簡單的dnode測試程式,放在跟testSerial.js同一個目錄:

var dnode = require('dnode');

dnode.connect(8000, 'localhost',  function(remote) {
    remote.restart(function(str) {
        console.log(str);
        process.exit();
    });
});

執行測試程式後,出現結果:

[flattened by searial:]
var dnode = require('dnode');

dnode.connect(8000, 'localhost',  function(remote) {
    remote.restart(function(str) {
        console.log(str);
        process.exit();
    });
});

[nested callbacks:]
var dnode = require('dnode');

dnode.connect(8000, 'localhost',  function(remote) {
    remote.restart(function(str) {
        console.log(str);
        process.exit();
    });
});

對照起來看,兩種寫法的結果其實是一樣的,但是利用serial.js,巢狀的callback結構就會消失。

不過這樣也只限於順序單純的狀況,如果函數執行的順序比較複雜(不只是一直線),還是需要用功能更完整的流程控制模組比較好,例如 https://github.com/caolan/async 。


2-6-3 組合

簡單地說,組合就是把函數執行的結果作為參數傳給另外一個函數,用這樣的形式把好幾個函數串接起來,然後一次執行。

例如:

var a = function(a){return a*3;};
var b = function(b){return b+5;};
console.log(a(b(2)));//結果是21,a(b(2))這樣就是組合

有些時候,可能需要預先做好處理一些payload的函數,然後可以依照需求任意組合。當然也可以直接組合來執行,但是有時候系統需要有彈性依照不定的需求來彈性組合,這樣就需要用一些方法來處理。

待續...
Comments