[Nodejs]非同步處理(async)

在之前的文章提過,Nodejs本身是一個單執行緒的語言,那麼在這樣的語言裡面,設計流程上需要注意什麼呢?有沒有什麼默認的規範必須遵守?

註:提及單執行緒的文章 --- [Nodejs]關於、介紹和筆記

此篇主要介紹非同步處理跟管理方法,大致上就是async和promise這兩種非同步用法,當然,絕對不是只有這兩種方法可以使用,不過大部分 npm社群網站中的套件都會優先符合這兩者。而Shach本身是習慣使用async,所以此篇還是主要以async為主,如果有哪邊寫錯還請高人指點迷津。

註:node js在v4版本後已經把promise納入核心模組中。

環境:Linux/Windows
版本:v4.X LTE
主要套件:async
輔助套件:
fs - 講解用,實際上使用並不會用到


前言

一般我們在寫nodejs的語言時,為了程序更像是多執行緒的流程,我們會將function切割開來丟到task queue中,並且盡量減少單一function的處理量,儘管看起來還是一條線的執行方式,不過真正在執行的時候,會因為function不斷切換的原因使的每個function在排隊的時候都可以更快的被執行到,而感受更為深刻。

但這時候遇到的問題就是如果function進入的一個無法取得回傳(return)的狀況下,要怎麼樣讓程式能夠繼續順利的進行呢?

舉例

example

var fs = require("fs");
var file_str = fs.readFile("./foo.txt");
console.log(file_str); // undefined

上面的範例是錯的, fs是官方提供的套件之一,使用fs.readFile時會調用非同步的方法,將執行的內容跑入你抓不到的範圍內,理所當然他的回傳值也就不會是在readFile這個function上。

example

var fs = require("fs");
var file_str = fs.readFile("./foo.txt", function(err,result){
    if(err) console.log(err);
    else console.log(result); // foo.txt content
});

以上才是正確的使用方法,給予一個function告知readFile執行結束後,將值傳入並執行該function。

名詞、變數

callback/cb - 回呼函數,在呼叫非同步函數時,傳入的function,以便非同步函數結束時將值傳入並執行
async - 非同步 或指async套件
sync - 同步
foo - 純粹就是個可以被取代的名字而已,你也可以把他改成qoo
argument - 函數所接收的參數

key word:
argument s - function作用域內部自帶變數,代表的是所收到的參數陣列。
註:arguments實際上並非是array,詳細請閱讀 [Nodejs]函數/類別(function) 

回呼函數(callback/cb)

首先你必須要知道的事情是,回呼函數有個默認的function model,即使在大部分的套件上都會是一樣的做法。

function(err, argument1, argument2, ...){

    // your code here

}


在這個function model上要注意的是,第一個被傳入的參數永遠是error,如果該非同步函數順利執行成功沒有錯誤,那麼第一個參數就會是undefined/null,而後第二個參數以後則為該非同步函數真正回傳的值。

--------------------------------------------------------------------------------------------------------------------------

實作非同步函數

var append_file_to_file = function(file1, file2, callback) {

    var file_combine = file1 + file2;



    // 執行非同步函數

    fs.writeFile("combine_result", file_combine, function(err, result) {

        // function執行結束,傳入結果並執行callback。

        callback(err, result);

    });

};

實作非同步函數時,請記得將callback定義在函數接收參數的最後一個,之後維護或是替function增加參數時,才不會有問題。所以我們也可以改成:

var append_file_to_file = function() {
    var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};
    var file_combine = "";
    for (var i = 0; i < (arguments.length - 1); i++) {
        file_combine += arguments[i];
    }

    // 執行非同步函數
    fs.writeFile("combine_result", file_combine, function(err, result) {
        // function執行結束,傳入結果並執行callback。
        callback(err, result);
    });
};

除了執行別人套件的非同步函數之外,我們自己也有辦法將函數變成非同步。

var div = function(num1, num2) {

    var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

    // 只是為了實現你不知道他什麼時候結束而已,沒有特別意義。

    var rand_num_1_to_1000_ms = (Math.floor(Math.random() * 1000) + 1);

    setTimeout(function() {

        if (!num2) {

        // 如果發生錯誤,則將錯誤傳入callback第一個參數

            callback("second argument must to be a number larger then 0");

        } else {

        // 正確執行時,將結果放於第二個參數之後

            callback(undefined, num1 / num2);

        }

    }, rand_num_1_to_1000_ms);

};

執行非同步函數

延續上一個主題實作的function div

div(1,0,function(err,result){

    if(err) console.log(err);

    else console.log(result);

});



/**

 * output:

 * "second argument must to be a number larger then 0"

 */

多層運用

div(1, 0, function(err, result) {

    if (err) console.log(err);

    else console.log(result);

    div(100, 10, function(err, result) {

        if (err) console.log(err);

        else console.log(result);

    });

});



/**

 * output:

 * "second argument must to be a number larger then 0"

 * 10

 */

就是這樣如此簡單,不過你可能會注意到,你已經開始出現所謂的 金字塔、callback地獄,不斷的使用非同步雖然會提升效能,但也大大增加了程式不易閱讀性。

--------------------------------------------------------------------------------------------------------------------------

本文重點,async的使用,可以增加非同步函數的管理維護和易讀性。

[Nodejs]關於、介紹和筆記裡面有提到,單執行緒的效能在於可以將閒置的時間拿去處理其他的任務,以任務分割時間處理達到多工的效果,因此將function輕量化是一個很重要的課題,Shach自己也會把冗長的程式碼分割成很多個小function,然後替每個function寫上註解以便管理不同的小區塊。用async時並沒有強制規定裡頭一定要執行的是非同步函數,只要記得告訴async你的function什麼時候結束就可以了。

以下介紹幾種常用的function,剩下的請拜讀 官方網站

在使用到async的地方引用async模組,並賦予到變數上。

var async = reuiqre("async");

並列執行(async.series)

async.series([function array/object], final_callback);

將輸入的function同時執行,並等待所有function回傳時,執行輸入給async.series的final_callback,特別注意的是如果其中一個function傳出error時,就會直接呼叫final_callback,而其他由async呼叫執行中的function則會停止。

這裡特別注意,裡頭所實作的function都會收到一個來自async函數傳入的callback,讓async知道你的function已經結束了,並且接收你的值後在final_callback還給你。

而callback參數傳入的位置一定會是在function所接收的參數最後一個,所以下面實作直接以此為原則來取得callback。

async.series([

    function() {

        var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

        div(1, 0, callback);

    },

    function() {

        var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

        div(100, 10, callback);

    }

], function(err, result_arr) {

    if (err) console.log(err);

    else console.log(result_arr);

});



/**

 * output:

 * "second argument must to be a number larger then 0"

 */

因為function array第一個function傳出了錯誤,所以直接呼叫final_callback。

async.series([

    function() {

        var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

        div(16, 4, callback);

    },

    function() {

        var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

        div(100, 10, callback);

    }

], function(err, result_arr) {

    if (err) console.log(err);

    else console.log(result_arr);

});



/**

 * output:

 * [4, 10]

 */

等待兩個function都正確執行結束後,呼叫final_callback,各個function的結果會依function在array中的順序填入result_arr裡頭。

arrar的方式如果你不喜歡,你也可以放入object。

async.series({

    div_1: function() {

        var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

        div(16, 4, callback);

    },

    div_2: function() {

        var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

        div(100, 10, callback);

    }

}, function(err, result_arr) {

    if (err) console.log(err);

    else console.log(result_arr);

});



/**

 * output:

 * { "div_1": 4, "div_2": 10 }

 */

回傳的結果會依function名稱輸出。

串列執行(async.parallel)


async.parallel([function array/object], final_callback);

將funtion array裡面的function逐一執行,上一個function結束(呼叫了async所給予的callback參數)後,就會執行下一個function。同series函數,只要其中一個發生error,就會直接呼叫final_callback。

這個函數可以有效的將非同步函數變成同步進行的感覺,但又不會浪費資源時間一直在做等待,而是等到確實執行完後才去接著做下一件事情。

async.parallel([

    function() {

        var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

        div(16, 4, callback);

    },

    function() {

        var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

        div(100, 10, callback);

    }

], function(err, result_arr) {

    if (err) console.log(err);

    else console.log(result_arr);

});



/**

 * output:

 * [4, 10]

 */

和series一樣,也可以將輸入的function array改為function object。

瀑布執行(async.waterfall)   我實在想不到更好的名詞解釋這個單字了...

async.waterfall([function array], final_callback);

waterfall就如同自面上意思,他是一層一層的向下執行,感覺上有點像是parallel,不一樣的是上一個function可以將callback結果傳遞給下一個執行的function。

async.waterfall([

    function() {

        var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

        div(16, 4, callback); // callback(undefined, 4);

    },

    function(last_function_result) {



        var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

        div(100, last_function_result, callback);

    }

], function(err, final_result) {

    if (err) console.log(err);

    else console.log(final_result);

});



/**

 * output:

 * 25

 */

和其他async的函數一樣,只要發生錯誤就會直接跳到final_callback,所以如果裡面的function是正確執行,而callback的第一個參數為undefined/null時,會直接將第二個以後的參數丟到下一個function裡頭,你callback裡面放n個,他就傳n.slice(1)個(排除第一個是error),就算只是傳undefined也會占用參數位置。

而final_callback結果的值,會是function array的最後一個function所傳入的值。

async.waterfall([

    function() {

        var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

        callback(undefined, 1, 2);

    },

    function(one, two) {

        var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

        callback(undefined, 3);

    },

    function(three) {

        var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

        callback(undefined, 4);

    }

], function(err, final_result) {

    if (err) console.log(err);

    else console.log(final_result);

});



/**

 * output:

 * 4

 */

個別執行(async.each)

async.each(array, iterator, final_callback);

用法和underscore的each或是array.eachOf很像,不一樣的是他是非同步執行的,可能你會覺得既然效果一樣為什麼要特別用非同步的方式處理呢?假設今天的array.length大於幾萬的時候,跑著each的那個function就會占用了大部分的運算時間,導致整支程式卡卡的,但若是用非同步處理就會和其他function一起排隊處理。

var arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];



async.each(arr, function(value) {

    var callback = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : function() {};

    console.log(value);

    callback();

}, function(err) {

    if (err) console.log(err);

    console.log("done");

});



/**

 * output:

 * 0

 * 1

 * 2

 * 3

 * 4

 * 5

 * 6

 * 7

 * 8

 * 9

 * 10

 * done

 */

iterator的function model中,第一個所接收的參數是陣列中的值,但不像Array.eachOf會給予index和array本身。最後一個參數一樣是callback,每次執行完記得要呼叫callback告知該function以處理結束。同其他async的函數,有一個發生錯誤就直接執行final_callback。

each函數是並列執行的,但官方同樣也提供串列執行(async.eachSeries),還有限制同時可以執行幾個的方法。

補充:

if(err) console.log(err);

在async裡面,將err放進if裡面可以最簡單的判斷這個非同步function是否出現錯誤,不管原始套件開發者是填null或是undefined,都會被判定成false,而不執行該行程式碼。
註: 未定義變數 (undefined, null, define)

沒有留言:

張貼留言