剖析Node.js異步編程中的回調(diào)與代碼設(shè)計(jì)模式
NodeJS 最大的賣(mài)點(diǎn)——事件機(jī)制和異步 IO,對(duì)開(kāi)發(fā)者并不是透明的。開(kāi)發(fā)者需要按異步方式編寫(xiě)代碼才用得上這個(gè)賣(mài)點(diǎn),而這一點(diǎn)也遭到了一些 NodeJS 反對(duì)者的抨擊。但不管怎樣,異步編程確實(shí)是 NodeJS 最大的特點(diǎn),沒(méi)有掌握異步編程就不能說(shuō)是真正學(xué)會(huì)了 NodeJS。本章將介紹與異步編程相關(guān)的各種知識(shí)。
在代碼中,異步編程的直接體現(xiàn)就是回調(diào)。異步編程依托于回調(diào)來(lái)實(shí)現(xiàn),但不能說(shuō)使用了回調(diào)后程序就異步化了。我們首先可以看看以下代碼。
function heavyCompute(n, callback) { var count = 0, i, j; for (i = n; i > 0; --i) { for (j = n; j > 0; --j) { count += 1; } } callback(count); } heavyCompute(10000, function (count) { console.log(count); }); console.log('hello');
100000000 hello
可以看到,以上代碼中的回調(diào)函數(shù)仍然先于后續(xù)代碼執(zhí)行。JS 本身是單線(xiàn)程運(yùn)行的,不可能在一段代碼還未結(jié)束運(yùn)行時(shí)去運(yùn)行別的代碼,因此也就不存在異步執(zhí)行的概念。
但是,如果某個(gè)函數(shù)做的事情是創(chuàng)建一個(gè)別的線(xiàn)程或進(jìn)程,并與JS主線(xiàn)程并行地做一些事情,并在事情做完后通知 JS 主線(xiàn)程,那情況又不一樣了。我們接著看看以下代碼。
setTimeout(function () { console.log('world'); }, 1000); console.log('hello');
hello world
這次可以看到,回調(diào)函數(shù)后于后續(xù)代碼執(zhí)行了。如同上邊所說(shuō),JS 本身是單線(xiàn)程的,無(wú)法異步執(zhí)行,因此我們可以認(rèn)為 setTimeout 這類(lèi) JS 規(guī)范之外的由運(yùn)行環(huán)境提供的特殊函數(shù)做的事情是創(chuàng)建一個(gè)平行線(xiàn)程后立即返回,讓 JS 主進(jìn)程可以接著執(zhí)行后續(xù)代碼,并在收到平行進(jìn)程的通知后再執(zhí)行回調(diào)函數(shù)。除了 setTimeout、setInterval 這些常見(jiàn)的,這類(lèi)函數(shù)還包括 NodeJS 提供的諸如 fs.readFile 之類(lèi)的異步 API。
另外,我們?nèi)匀换氐?JS 是單線(xiàn)程運(yùn)行的這個(gè)事實(shí)上,這決定了 JS 在執(zhí)行完一段代碼之前無(wú)法執(zhí)行包括回調(diào)函數(shù)在內(nèi)的別的代碼。也就是說(shuō),即使平行線(xiàn)程完成工作了,通知 JS 主線(xiàn)程執(zhí)行回調(diào)函數(shù)了,回調(diào)函數(shù)也要等到 JS 主線(xiàn)程空閑時(shí)才能開(kāi)始執(zhí)行。以下就是這么一個(gè)例子。
function heavyCompute(n) { var count = 0, i, j; for (i = n; i > 0; --i) { for (j = n; j > 0; --j) { count += 1; } } } var t = new Date(); setTimeout(function () { console.log(new Date() - t); }, 1000); heavyCompute(50000);
8520
可以看到,本來(lái)應(yīng)該在1秒后被調(diào)用的回調(diào)函數(shù)因?yàn)?JS 主線(xiàn)程忙于運(yùn)行其它代碼,實(shí)際執(zhí)行時(shí)間被大幅延遲。
代碼設(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); }
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); }
/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. } });
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ù)雜度。
- Node.js基礎(chǔ)入門(mén)之回調(diào)函數(shù)及異步與同步詳解
- 如何將Node.js中的回調(diào)轉(zhuǎn)換為Promise
- Node.js 回調(diào)函數(shù)實(shí)例詳解
- node.js實(shí)現(xiàn)回調(diào)的方法示例
- 深入理解Node.js 事件循環(huán)和回調(diào)函數(shù)
- node.js回調(diào)函數(shù)之阻塞調(diào)用與非阻塞調(diào)用
- 我的Node.js學(xué)習(xí)之路(三)--node.js作用、回調(diào)、同步和異步代碼 以及事件循環(huán)
- Node.js Continuation Passing Style( CPS與回調(diào))
相關(guān)文章
如何利用nodejs自動(dòng)定時(shí)發(fā)送郵件提醒(超實(shí)用)
這篇文章主要給大家介紹了關(guān)于如何利用nodejs實(shí)現(xiàn)自動(dòng)定時(shí)發(fā)送郵件提醒的相關(guān)資料,這個(gè)功能非常實(shí)用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12Node環(huán)境中JS代碼缺少window對(duì)象的原因和解決方案
你可能會(huì)在某些情況下需要在Node環(huán)境下運(yùn)行JavaScript代碼,但你也可能會(huì)遇到一個(gè)常見(jiàn)的問(wèn)題:缺少window環(huán)境,在本文中,我們將深入探討這個(gè)問(wèn)題的原因,并提供解決方案,需要的朋友可以參考下2023-08-08iOS + node.js使用Socket.IO框架進(jìn)行實(shí)時(shí)通信示例
本篇文章主要介紹了iOS + node.js使用Socket.IO框架進(jìn)行實(shí)時(shí)通信示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04Node.js實(shí)現(xiàn)批量去除BOM文件頭
這篇文章主要介紹了Node.js實(shí)現(xiàn)批量去除BOM文件頭,本文直接給出實(shí)現(xiàn)代碼,需要的朋友可以參考下2014-12-12nodejs微信開(kāi)發(fā)之自動(dòng)回復(fù)的實(shí)現(xiàn)
這篇文章主要介紹了nodejs微信開(kāi)發(fā)之自動(dòng)回復(fù)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03我的Node.js學(xué)習(xí)之路(三)--node.js作用、回調(diào)、同步和異步代碼 以及事件循環(huán)
本篇文章主要介紹了node.js的幾個(gè)重要的知識(shí)點(diǎn):node.js作用、回調(diào)、同步和異步代碼 以及事件循環(huán)2014-07-07node.js中ws模塊創(chuàng)建服務(wù)端與客戶(hù)端實(shí)例代碼
在Node.js中提供了http模塊與https模塊,專(zhuān)用于創(chuàng)建HTTP服務(wù)器、HTTP客戶(hù)端,以及HTTPS服務(wù)器及HTTPS客戶(hù)端,同時(shí)實(shí)現(xiàn)這些服務(wù)器端與客戶(hù)端之中所需進(jìn)行的處理,下面這篇文章主要給大家介紹了關(guān)于node.js中ws模塊創(chuàng)建服務(wù)端與客戶(hù)端的相關(guān)資料,需要的朋友可以參考下2023-05-05