NodeJS學習之異步編程

NodeJS -- 異步編程

 NodeJS最大的賣點--事件機制和異步IO,對開發者並不透明編程

代碼設計模式

異步編程有不少特有的代碼設計模式,爲了實現一樣的功能,使用同步方式和異步方式編寫代碼會有很大差別,如下舉例。
一、函數返回值
使用一個函數的輸出做爲另外一個函數的輸入是常見的需求,在同步方式下通常如下述方式編寫代碼:
var output = fn1(fn2('input'));
// Do something;

在異步方式下,因爲函數執行結果不是經過返回值,而是經過回調函數傳遞,所以通常按如下方式編寫代碼:設計模式

fn2('input', function(output2) {
    fn1(output2, function(output1) {
        //Do something.
    });
});
:這種方式就是一個函數套一個函數,層級變多了,就很麻煩了
二、遍歷數組
在遍歷數組時,使用某個函數依次對數據成員作一些處理也是常見的需求,若函數是同步執行,通常如下列方式:
var len = arr.length,
      i = 0;
for(;i < len; ++i) {
    arr[i] = sync(arr[i]);
}
//All array items have processed.

如果異步執行,以上代碼就沒法保證循環結束後全部數組成員都處理完畢了,若是數組成員必須一個接一個串行處理,則按如下方式編寫代碼:數組

(function next(i, len, callback) {
    if(i < len) {
        async(arr[i], function(value) {
            arr[i] = value;
            next(i + 1, len, callback);
        });
    } else {
        callback();
    }
}(0, arr.length, function() {
    // All array items have processed.
}));

能夠看到,在異步函數執行一次並返回執行結果後才傳入下一個數組成員並開始下一輪執行,直到全部數組成員處理完畢後,經過回調的方式觸發後續代碼執行。服務器

若數組成員能夠並行處理,但後續代碼仍然須要全部數組成員處理完畢後才能執行的話,則異步代碼調整成如下形式:dom

(function(i, len, count, callback) {
    for(;i < len; ++i) {
        (function(i) {
            async(arr[i], function(value) {
                arr[i] = value;
                if(++count === len) {
                    callback();
                }
            });
        }(i));
    }
}(0, arr.length, 0, function() {
    //All array items have processed.
}));

以上代碼並行處理全部成員,並經過計數器變量來判斷何時全部數組成員都處理完畢了異步

三、異常處理
JS自身提供的異常捕獲和處理機制 -- try...catch...,只能用於同步執行的代碼。
但,因爲異步函數會打斷代碼執行路徑,異步函數執行過程當中以及執行以後產生的異常 冒泡到執行路徑被打斷的位置時,若是一直沒有遇到try語句,就做爲一個全局異常拋出。例:
function async(fn, callback) {
    // Code execution path breaks here.
    setTimeout(function() {
        callback(fn());
    }, 0);
}

try {
    async(null, function(data) {
        // Do something.
    });
} catch(err) {
    console.log('Error: %s', err.message);
}
-----------------------Console----------------------------
E:\Language\Javascript\NodeJS\try_catch.js:25
        callback(fn());
                 ^

TypeError: fn is not a function
    at null._onTimeout (E:\Language\Javascript\NodeJS\try_catch.js:25:18)
    at Timer.listOnTimeout (timers.js:92:15)

由於代碼執行路徑被打斷了,咱們就須要在異常冒泡到斷點以前用try語句把異常捕獲注,並經過回調函數傳遞被捕獲的異常,改造:async

function async(fn, callback) {
    // Code execution path breaks here.
    setTimeout(function() {
        try {
            callback(null, fn());
        } catch(err) {
            callback(err);
        }
    }, 0);
}

async(null, function(err, data) {
    if(err) {
        console.log('Error: %s', err.message);
    } else {
        // Do something
    }
})
----------------------Console----------------------
Error: fn is not a function
異常再次被捕獲住。
在NodeJS中,幾乎全部的異步API都按照以上方式設計,回調函數中第一個參數都是err,所以咱們在編寫本身的異步函數時,也能夠按照這種方式來處理異常,與NodeJS的設計風格保持一致。
但,咱們的代碼一般都是作一些事情,調用一個函數,而後再作一些事情,調用一個函數,若使用同步代碼,只須要在入口點寫一個try...catch...語句便可捕獲全部冒泡的異常,若使用異步代碼,那就呵呵了,就像下面,只調用三次異步函數:
function main(callback) {
    // Do something.
    asyncA(function(err, data) {
        if(err) {
            callback(err);
        } else {
            // Do something.
            asyncB(function(err, data) {
                if(err) {
                    callback(err);
                } else {
                    // Do something.
                    asyncC(function(err, data) {
                        if(err) {
                            callback(err);
                        } else {
                            // Do something.
                            callback(null);
                        }
                    });
                }
            });
        }
    });
}
main(function(err) {
    if(err) {
        // Deal with exception.
    }
});

回調函數已經讓代碼變得複雜了,而異步之下的異常處理更加重了代碼的複雜度,幸虧,NodeJS提供了一些解決方案。異步編程

NodeJS提供了domain模塊,能夠簡化異步代碼的異常處理。
域,簡單講就是一個JS運行環境,在一個運行環境中,若是一個異常沒有被捕獲,將做爲一個全局異常拋出,NodeJS經過process對象提供了捕獲全局異常的方法,例:
process.on('uncaughtException', function(err) {
    console.log('Error: %s', err.message);
});

setTimeout(function(fn) {
    fn();
});
------------------------Console--------------------
Error: fn is not a function
全局異常已被捕獲,但大多數異常咱們但願儘早捕獲,並根據結果決定代碼執行路徑。
用HTTP服務器代碼做例子:
function async(req, callback) {
    // Do something.
    asyncA(req, function(err, data) {
        if(err) {
            callback(err);
        } else {
            // Do something.
            asyncB(req, function(err, data) {
                if(err) {
                    callback(err);
                } else {
                    // Do something.
                    asyncC(req, function(err, data) {
                        if(err) {
                            callback(err);
                        } else {
                            // Do something.
                            callback(null, data);
                        }
                    });
                }
            });
        }
    });
}

http.createServer(function(req, res) {
    async(req, function(err, data) {
        if(err) {
            res.writeHead(500);
            res.end();
        } else {
            res.writeHead(200);
            res.end();
        }
    });
});

以上將請求對象交給異步函數處理,再根據處理結果返回響應,這裏採用了使用回調函數傳遞異常的方案,所以async函數內部若再多幾個異步函數調用的話,代碼就更難看了,爲了讓代碼好看點,能夠在沒處理一個請求時,使用domain模塊建立一個子域,在子域內運行的代碼能夠隨意拋出異常,而這些異常能夠經過子域對象的error事件統一捕獲,改造:函數

function async(req, callback) {
    // Do something.
    asyncA(req, function(data) {
        // Do something.
        asyncB(req, function(data) {
            // Do something.
            asyncC(req, function(data) {
                // Do something.
                callback(data);
            });
        });
    });
}

http.createServer(function(req, res) {
    var d = domain.create();

    d.on('error', function() {
        res.writeHead(500);
        res.end();
    });

    d.run(function() {
        async(req, function(data) {
            res.writeHead(200);
            res.end(data);
        });
    });
});
注意:
不管是經過process對象的uncaughtException事件捕獲到全局異常,仍是經過子域對象的error事件捕獲到子域異常,在NodeJS官方文檔均強烈建議處理完異常後應當即重啓程序,而不是讓程序繼續運行,由於發生異常後程序處於一個不肯定運行狀態,若不退出,可能會發生嚴重內存泄漏,也可能表現得很奇怪

注:JS的throw...tyr...catch異常處理機制並不會致使內存泄漏和使程序執行出乎意料,而是由於NodeJS並非純粹的JS,NodeJS裏大量的API內部是用C/C++實現的,所以NodeJS程序運行過程當中,代碼執行路徑穿梭於JS引擎內外部,而JS異常拋出機制可能打斷正常代碼的執行流程,致使C/C++部分的代碼表現異常,進而致使內存泄漏。spa

所以,使用uncaughtException或domain捕獲異常,代碼執行路徑裏涉及到了C/C++部分的代碼時,若不能肯定是否會致使內存泄漏等問題,最好在處理完異常後重啓程序比較穩當,而使用try語句捕獲的異常通常是JS自己的異常,不用擔憂上述問題

相關文章
相關標籤/搜索