NodeJS學習之異步編程


NodeJS -- 異步編程

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

代碼設計模式

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

在異步方式下,由於函數執行結果不是通過返回值,而是通過回調函數傳遞,因此一般按以下方式編寫代碼:

fn2('input', function(output2) {
    fn1(output2, function(output1) {
        //Do something.
    });
});
:這種方式就是一個函數套一個函數,層級變多了,就很麻煩了
2、遍歷數組
在遍歷數組時,使用某個函數依次對數據成員做一些處理也是常見的需求,若函數是同步執行,一般以下列方式:
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.
}));

可以看到,在異步函數執行一次並返回執行結果后才傳入下一個數組成員並開始下一輪執行,直到所有數組成員處理完畢后,通過回調的方式觸發后續代碼執行。

若數組成員可以並行處理,但后續代碼仍然需要所有數組成員處理完畢后才能執行的話,則異步代碼調整成以下形式:

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

以上代碼並行處理所有成員,並通過計數器變量來判斷什么時候所有數組成員都處理完畢了

3、異常處理
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語句把異常捕獲注,並通過回調函數傳遞被捕獲的異常,改造:

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++部分的代碼表現異常,進而導致內存泄漏。

因此,使用uncaughtException或domain捕獲異常,代碼執行路徑里涉及到了C/C++部分的代碼時,若不能確定是否會導致內存泄漏等問題,最好在處理完異常后重啟程序比較妥當,而使用try語句捕獲的異常一般是JS本身的異常,不用擔心上述問題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM