Nodejs 代碼設(shè)計(jì)模式

2018-09-28 20:30 更新

代碼設(shè)計(jì)模式

異步編程有很多特有的代碼設(shè)計(jì)模式,為了實(shí)現(xiàn)同樣的功能,使用同步方式和異步方式編寫(xiě)的代碼會(huì)有很大差異。以下分別介紹一些常見(jiàn)的模式。

函數(shù)返回值

使用一個(gè)函數(shù)的輸出作為另一個(gè)函數(shù)的輸入是很常見(jiàn)的需求,在同步方式下一般按以下方式編寫(xiě)代碼:

var output = fn1(fn2('input'));
// Do something.

而在異步方式下,由于函數(shù)執(zhí)行結(jié)果不是通過(guò)返回值,而是通過(guò)回調(diào)函數(shù)傳遞,因此一般按以下方式編寫(xiě)代碼:

fn2('input', function (output2) {
    fn1(output2, function (output1) {
        // Do something.
    });
});

可以看到,這種方式就是一個(gè)回調(diào)函數(shù)套一個(gè)回調(diào)函多,套得太多了很容易寫(xiě)出>形狀的代碼。

遍歷數(shù)組

在遍歷數(shù)組時(shí),使用某個(gè)函數(shù)依次對(duì)數(shù)據(jù)成員做一些處理也是常見(jiàn)的需求。如果函數(shù)是同步執(zhí)行的,一般就會(huì)寫(xiě)出以下代碼:

var len = arr.length,
    i = 0;

for (; i < len; ++i) {
    arr[i] = sync(arr[i]);
}

// All array items have processed.

如果函數(shù)是異步執(zhí)行的,以上代碼就無(wú)法保證循環(huán)結(jié)束后所有數(shù)組成員都處理完畢了。如果數(shù)組成員必須一個(gè)接一個(gè)串行處理,則一般按照以下方式編寫(xiě)異步代碼:

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

可以看到,以上代碼在異步函數(shù)執(zhí)行一次并返回執(zhí)行結(jié)果后才傳入下一個(gè)數(shù)組成員并開(kāi)始下一輪執(zhí)行,直到所有數(shù)組成員處理完畢后,通過(guò)回調(diào)的方式觸發(fā)后續(xù)代碼的執(zhí)行。

如果數(shù)組成員可以并行處理,但后續(xù)代碼仍然需要所有數(shù)組成員處理完畢后才能執(zhí)行的話(huà),則異步代碼會(huì)調(diào)整成以下形式:

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

可以看到,與異步串行遍歷的版本相比,以上代碼并行處理所有數(shù)組成員,并通過(guò)計(jì)數(shù)器變量來(lái)判斷什么時(shí)候所有數(shù)組成員都處理完畢了。

異常處理

JS 自身提供的異常捕獲和處理機(jī)制——try..catch..,只能用于同步執(zhí)行的代碼。以下是一個(gè)例子。

function sync(fn) {
    return fn();
}

try {
    sync(null);
    // Do something.
} catch (err) {
    console.log('Error: %s', err.message);
}

-- Console ------------------------------
Error: object is not a function

可以看到,異常會(huì)沿著代碼執(zhí)行路徑一直冒泡,直到遇到第一個(gè) try 語(yǔ)句時(shí)被捕獲住。但由于異步函數(shù)會(huì)打斷代碼執(zhí)行路徑,異步函數(shù)執(zhí)行過(guò)程中以及執(zhí)行之后產(chǎn)生的異常冒泡到執(zhí)行路徑被打斷的位置時(shí),如果一直沒(méi)有遇到 try 語(yǔ)句,就作為一個(gè)全局異常拋出。以下是一個(gè)例子。

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 ------------------------------
/home/user/test.js:4
        callback(fn());
                 ^
TypeError: object is not a function
    at null._onTimeout (/home/user/test.js:4:13)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)

因?yàn)榇a執(zhí)行路徑被打斷了,我們就需要在異常冒泡到斷點(diǎn)之前用 try 語(yǔ)句把異常捕獲住,并通過(guò)回調(diào)函數(shù)傳遞被捕獲的異常。于是我們可以像下邊這樣改造上邊的例子。

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: object is not a function

可以看到,異常再次被捕獲住了。在 NodeJS 中,幾乎所有異步 API 都按照以上方式設(shè)計(jì),回調(diào)函數(shù)中第一個(gè)參數(shù)都是 err。因此我們?cè)诰帉?xiě)自己的異步函數(shù)時(shí),也可以按照這種方式來(lái)處理異常,與 NodeJS 的設(shè)計(jì)風(fēng)格保持一致。

有了異常處理方式后,我們接著可以想一想一般我們是怎么寫(xiě)代碼的。基本上,我們的代碼都是做一些事情,然后調(diào)用一個(gè)函數(shù),然后再做一些事情,然后再調(diào)用一個(gè)函數(shù),如此循環(huán)。如果我們寫(xiě)的是同步代碼,只需要在代碼入口點(diǎn)寫(xiě)一個(gè) try 語(yǔ)句就能捕獲所有冒泡上來(lái)的異常,示例如下。

function main() {
    // Do something.
    syncA();
    // Do something.
    syncB();
    // Do something.
    syncC();
}

try {
    main();
} catch (err) {
    // Deal with exception.
}

但是,如果我們寫(xiě)的是異步代碼,就只有呵呵了。由于每次異步函數(shù)調(diào)用都會(huì)打斷代碼執(zhí)行路徑,只能通過(guò)回調(diào)函數(shù)來(lái)傳遞異常,于是我們就需要在每個(gè)回調(diào)函數(shù)里判斷是否有異常發(fā)生,于是只用三次異步函數(shù)調(diào)用,就會(huì)產(chǎn)生下邊這種代碼。

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

可以看到,回調(diào)函數(shù)已經(jīng)讓代碼變得復(fù)雜了,而異步方式下對(duì)異常的處理更加劇了代碼的復(fù)雜度。如果 NodeJS 的最大賣(mài)點(diǎn)最后變成這個(gè)樣子,那就沒(méi)人愿意用 NodeJS 了,因此接下來(lái)會(huì)介紹 NodeJS 提供的一些解決方案。

以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)