Node.js Async套件

關於Node.js的 Async ,針對會使用這個套件的原因大概描述一下:

一、狀況

寫作Node.js專案時,由於單執行序與事件驅動的特性,勢必須要面對依序執行函式,或要拜訪每個陣列元素又要避免佔用執行序太久的狀況,先說明一下案例

1.依序執行function:如果碰到需要執行多個function,且每個function之間有前後關係時,如果不使用Promise語法或Async套件,你就只能一層又一層的寫下去,類似下面這樣
function a(callBack){
    return callBack(null , "a done");
}

function b(callBack){
    return callBack(null , "b done");
}

function c(callBack){
    return callBack(null , "c done");
}

a(function(error , rtn){
    console.log(rtn);
    b(function(error , rtn){
        console.log(rtn);
        c(function(error , rtn){
            console.log(rtn);
        });
    });
});

//output:
a done
b done
c done
如同上面範例看到的,為了保證依序執行,必須這樣一層一層的寫下去,請想想一下四層或五層,中間再加上一些邏輯判斷,有些要執行有些又不需要執行等,原始碼會變得很複雜難以閱讀跟維護。

2.執行迴圈辦法陣列物件:
比如你有一個物件陣列,要一一比對每個值,挑選出需要的物件

var array1 = [{ type:1,name:"p1"} , { type:2,name:"p2"} , { type:1,name:"p3"}];
var array2 = [];
array1.forEach(function(item){
    if(item.type === 1){
        array2.push(item);
    }
});
console.log(array2);

//output
[ { type: 1, name: 'p1' }, { type: 1, name: 'p3' } ]

上面語法看起來似乎沒什麼問題,不過重點是array的forEach(),或者是map()這些功能相近的function,都是同步(Synchronous)的,也就是要等到forEach()執行完才會繼續執行下一行指令,而Node.js是單執行序的,也就是說所有如果每個array的元素需要運算很久或array中包含太多的項目,就可能造成其他需求等待的時間過長。

二、來說說Async

關於Async,他的說明文件連結在這邊
使用套件管理工具npm或yarn安裝,功能包含兩大類型,一類是用於流程控制(control flow),另一類則是處理集合操作(collections),剛好對應上面兩個問題,下面會各提出一個函式來介紹。

1.流程控制:多個函式需依序執行時
當你需要依序執行多個function時,可以使用async的waterfall,傳入要依序執行的function陣列與第二個參數callBack的函式,async會依序執行陣列中的函式,一個完成後才會呼叫下一個,直到每個都執行完畢,當任一個於回呼時傳入錯誤,則callBack會被觸發,並收到拋出的錯誤,不然只有最後一個函式執行回呼時,才會進入callBack,請參考範例:

var async = require("async");

async.waterfall([
    function(callback) {
        callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback) {
        // arg1 now equals 'one' and arg2 now equals 'two'
        console.log(arg1+"/"+arg2);
        callback(null, 'three');
    },
    function(arg1, callback) {
        // arg1 now equals 'three'
        console.log(arg1);
        callback(null, 'done');
    }
], function (err, result) {
    // result now equals 'done'
    console.log(result);
});

//output
one/two
three
done

參考上面範例,會看到將參數一個個往後傳遞,一直到最後一個函式呼叫callback時,觸發waterfall的第二個參數並取得err與result,中間任何function如果於callback時第一個參數傳入任何值,整個程序會馬上結束。

利用callback傳入第一個參數來結束程序的機制,某些時候並不是因為發生錯誤,可能只是因為工作已經處理完,可以跳過後面的function,所以打算結束程序,為了跟發生錯誤時做區別,可以如下面這樣:
var async = require("async");


function check(a1 , a2){
    async.waterfall([
        function(callback) {
            if(!a1 || !a2){
                return callback(new Error("需要傳入a1與a2"));
            }
            if(a1===a2){
                //提前結束
                return callback(true);
            }
            return callback();
        },
        function(callback) {
            return callback(null , "執行到第二個function了");
        }
    ], function (done, result) {
        if(done === true){
            return console.log("第一個function就正常跳出,無錯誤發生");
        }
        else if(done instanceof Error){
            //done為一個Error物件
            return console.log(done.message);
        }
        // result now equals 'done'
        return console.log(result);
    });
}

check();
check(1,1);
check(1,2);

//output
需要傳入a1與a2
第一個function就正常跳出,無錯誤發生
執行到第二個function了
就像範例看到的,於async.waterfall的callback中,透過檢查第一個傳入參數的值或類型,判斷程序是正常執行完畢或是因為意外而結束。

async除了依序執行的waterfall以外,也有不管function執行順序的parallel或需判斷條件執行的各類與流程控制有關的function,有機會再介紹。

2.集合處理(collections)
像是Array的map、forEach,只是要採用異步方式,避免處理多筆數的集合時,佔用處理序太久,最簡單的就是each函式,參考範例如下:
var async = require("async");

var array1 = ["a","b","c","d"];

async.each(array1 ,
function(value , cb){
    console.log(value);
    return cb();
},
function(error){
    if(error){
        //發生錯誤
    }
    else{
        //完成
        console.log("done")
    }
})

//output
a
b
c
d
done
需注意的是each會依順序執行,卻不保證會依據順序結束,下面是故意讓第一個參數較晚結束的狀況

var async = require("async");

var array1 = ["a","b","c","d"];

async.each(array1 ,
function(value , cb){
    console.log("start "+value);
    if(value === "a"){
        wait(1000 , function(){
            console.log("end "+value);
            return cb();
        });
    }
    else{
        console.log("end "+value);
        return cb();
    }
},
function(error){
    if(error){
        //發生錯誤
    }
    else{
        //完成
        console.log("done")
    }
});


function wait(value , callback){
    setTimeout(callback, value);
}

//output
start a
start b
end b
start c
end c
start d
end d
end a
done


可以看到完成雖然a先執行,可是b、c、d在a還未執行完之前就依序執行並完成了。
那如果有需要一個處理完成才處理下一個值時該怎麼辦呢?async提供了eachSeries來處理這個問題,eachSeries只會有一個異步程序在執行,所以可以控制其按順序一個完成後才執行下一個,下面範例是只修改each為eachSeries,執行程式可以明顯看到第二項b會等到end a列印出來後才執行

var async = require("async");

var array1 = ["a","b","c","d"];

async.eachSeries(array1 ,
function(value , cb){
    console.log("start "+value);
    if(value === "a"){
        wait(1000 , function(){
            console.log("end "+value);
            return cb();
        });
    }
    else{
        console.log("end "+value);
        return cb();
    }
},
function(error){
    if(error){
        //發生錯誤
    }
    else{
        //完成
        console.log("done")
    }
});


function wait(value , callback){
    setTimeout(callback, value);
}
//output
start a
end a
start b
end b
start c
end c
start d
end d
done

請務必自己嘗試看看喔,會有比較直觀的認識。

由文件中還可以看到另一個eachLimit函式,其差別在於可以指定同時有多少個異步程序在執行,當然node是單執行序的,這邊所要表示的是說,同時允許多少個執行去排隊等待,牽扯到node基本認識的部份有機會再談,總之先想成eachLimit可以設定同一個時間允許幾個項目執行,eachSeries就像是指定Limit值為1的eachLimit一樣。

async的集合處理函式還有很多類型,比如會有回傳值的map等,基本上也都提供了Limit與Series兩個差異化的函式,如mapLimit與mapSeries,意義上與剛剛介紹的相同,大家可以自己嘗試看看。


留言

這個網誌中的熱門文章

天雨粟、鬼夜哭、思念漫太古。

蘇打綠 - 御花園

Nodejs Base64 Url Safe