Asynchronous

有許多人無法理解同步和非同步的差別,這邊以去餐廳吃飯來舉個例子。

同步

排隊點餐,點完餐,站在櫃檯等餐來,領完餐才離開櫃檯。

非同步

排隊點餐,點完餐拿著號碼牌走掉,櫃檯繼續服務下個客人,餐點送來依號碼牌取餐。

從上述例子來看,同步處理是線性的,一件事做完才會做下一件事;但非同步是服務生在點單,而後面的廚房同時在備餐。非同步處理與平行處理不同,平行處理指的是櫃檯有很多個服務生,可以同時點一堆單,但是怎麼取餐還是回到同步或非同步的處理。

在Node.js,所有的I/O都是非同步的,也就是I/O call出去,並不會等到I/O回來才接著做,而是會接著做下面的task。要寫Node.js一定要先有這個認知,例如:

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

以上的file_str是未定義,因為readFile是非同步,所以檔案還沒讀完就已經執行到console.log。正確的做法是:

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
});

但若是一連串的非同步操作,例如:我要讀的檔案A被記錄在檔案B內,而檔案B又必須從網路上取得,網路的URL又被記錄在設定檔案。這樣會一層接著一層的callback function。有一個專有名詞叫做回呼地獄(callback hell)。在javascript的ES2016出了一個功能是Promise,用來解決回呼地獄,而ES2017出了async/await來使Promise更容易調用。

但即便是async/await我還是覺得那個使用方式很繁瑣,更重要的是error handle不是很直覺,必須要try/catch才有辦法。因此我個人更偏好使用外部的async module:

npm install async

在async模組中提供了許多流程控制的API,例如waterfall/series/parallel等,這邊舉waterfall作為例子。

const Async = require('async');

function asyncio(msg, next, err, res) {
    setTimeout(function(){
        console.log('asyncio: '+msg);
        next(err, res);
    }, 1000);
}

function done(err, final_result) {
    console.log('!DONE!');
    console.log('!ERROR! '+err);
    console.log('!RESULT! '+final_result);
}

function cb(err, final_result) {
    console.log('!CALLBACK! '+final_result+', err: '+err);
}

這邊先準備一個偽裝的非同步操作asyncio,雖然只是setTimout,但其實非同步API做的事也大同小異。done則是最後的error handler。首先說明waterfall的運作原理。

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

原型是如上所述,waterfall會將列表內的function按照順序執行,而每一個function會有一個控制流程用的API,用來傳遞錯誤和結果。

function(err, result)

若是任何一個流程控制的API有收到error,那個就會執行waterfall最後的的error handler。

function wf_step1_stop(callback) {
    Async.waterfall([
        function _step1(next) {
            err1 = new Error('Err step1');
            asyncio('step1', next, err1, 'result step1');
        },
        function _step2(result1, next) {
            err2 = new Error('Err step2');
            asyncio(result1+' in step2', next, err2, 'result step2');
        },
        function _step3(result2, next) {
            asyncio(result2+' in step3', callback, null, 'result in step3');
        }
    ], done);
}
wf_step1_stop(cb);

上面的wf_step1_stop執行到step1就因為錯誤而結束了,所以在step3的callback並不會被執行到。想要執行callback就必須確保step1和step2能夠正確執行。

function wf_callback_without_done(callback) {
    Async.waterfall([
        function _step1(next) {
            asyncio('step1', next, null, 'result step1');
        },
        function _step2(result1, next) {
            asyncio(result1+' in step2', next, null, 'result step2');
        },
        function _step3(result2, next) {
            asyncio(result2+' in step3', callback, null, 'result in step3');
        }
    ], done);
}

但這樣的做法並不好,在async的文件中提到,若是callback在列表內的function就被執行到,必須要在前面加上return才可以避免在一些corner case發生不可預期的行為。也因此,正確的practice應該是把callback放在done內執行,才可以確保既有error handle又能夠處理callback的邏輯。

function wf_done(callback) {
    Async.waterfall([
        function _step1(next) {
            asyncio('step1', next, null, 'result step1');
        },
        function _step2(result1, next) {
            asyncio(result1+' in step2', next, null, 'result step2');
        },
        function _step3(result2, next) {
            asyncio(result2+' in step3', next, null, 'result in step3');
        }
    ], (err, res) => {console.log('!DONE! err: '+err+', result: '+res); callback(err, res); });
}

在async的文件中還有介紹許多關於流程控制的API,請看註1。另外可以透過註2更加理解非同步的流程控制基礎。

[1] async document [2] control flow

results matching ""

    No results matching ""