国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

剖析Node.js異步編程中的回調與代碼設計模式

2019-11-20 10:35:29
字體:
來源:轉載
供稿:網友

NodeJS 最大的賣點――事件機制和異步 IO,對開發者并不是透明的。開發者需要按異步方式編寫代碼才用得上這個賣點,而這一點也遭到了一些 NodeJS 反對者的抨擊。但不管怎樣,異步編程確實是 NodeJS 最大的特點,沒有掌握異步編程就不能說是真正學會了 NodeJS。本章將介紹與異步編程相關的各種知識。

在代碼中,異步編程的直接體現就是回調。異步編程依托于回調來實現,但不能說使用了回調后程序就異步化了。我們首先可以看看以下代碼。

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');
100000000hello

可以看到,以上代碼中的回調函數仍然先于后續代碼執行。JS 本身是單線程運行的,不可能在一段代碼還未結束運行時去運行別的代碼,因此也就不存在異步執行的概念。

但是,如果某個函數做的事情是創建一個別的線程或進程,并與JS主線程并行地做一些事情,并在事情做完后通知 JS 主線程,那情況又不一樣了。我們接著看看以下代碼。

setTimeout(function () { console.log('world');}, 1000);console.log('hello');
helloworld


這次可以看到,回調函數后于后續代碼執行了。如同上邊所說,JS 本身是單線程的,無法異步執行,因此我們可以認為 setTimeout 這類 JS 規范之外的由運行環境提供的特殊函數做的事情是創建一個平行線程后立即返回,讓 JS 主進程可以接著執行后續代碼,并在收到平行進程的通知后再執行回調函數。除了 setTimeout、setInterval 這些常見的,這類函數還包括 NodeJS 提供的諸如 fs.readFile 之類的異步 API。

另外,我們仍然回到 JS 是單線程運行的這個事實上,這決定了 JS 在執行完一段代碼之前無法執行包括回調函數在內的別的代碼。也就是說,即使平行線程完成工作了,通知 JS 主線程執行回調函數了,回調函數也要等到 JS 主線程空閑時才能開始執行。以下就是這么一個例子。

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


可以看到,本來應該在1秒后被調用的回調函數因為 JS 主線程忙于運行其它代碼,實際執行時間被大幅延遲。

代碼設計模式
異步編程有很多特有的代碼設計模式,為了實現同樣的功能,使用同步方式和異步方式編寫的代碼會有很大差異。以下分別介紹一些常見的模式。

函數返回值
使用一個函數的輸出作為另一個函數的輸入是很常見的需求,在同步方式下一般按以下方式編寫代碼:

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

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

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

(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..,只能用于同步執行的代碼。以下是一個例子。

function sync(fn) { return fn();}try { sync(null); // Do something.} catch (err) { console.log('Error: %s', err.message);}
Error: object is not a function

可以看到,異常會沿著代碼執行路徑一直冒泡,直到遇到第一個 try 語句時被捕獲住。但由于異步函數會打斷代碼執行路徑,異步函數執行過程中以及執行之后產生的異常冒泡到執行路徑被打斷的位置時,如果一直沒有遇到 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);}
/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)

因為代碼執行路徑被打斷了,我們就需要在異常冒泡到斷點之前用 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. }});
Error: object is not a function

可以看到,異常再次被捕獲住了。在 NodeJS 中,幾乎所有異步 API 都按照以上方式設計,回調函數中第一個參數都是 err。因此我們在編寫自己的異步函數時,也可以按照這種方式來處理異常,與 NodeJS 的設計風格保持一致。

有了異常處理方式后,我們接著可以想一想一般我們是怎么寫代碼的。基本上,我們的代碼都是做一些事情,然后調用一個函數,然后再做一些事情,然后再調用一個函數,如此循環。如果我們寫的是同步代碼,只需要在代碼入口點寫一個 try 語句就能捕獲所有冒泡上來的異常,示例如下。

function main() { // Do something. syncA(); // Do something. syncB(); // Do something. syncC();}try { main();} catch (err) { // Deal with exception.}

但是,如果我們寫的是異步代碼,就只有呵呵了。由于每次異步函數調用都會打斷代碼執行路徑,只能通過回調函數來傳遞異常,于是我們就需要在每個回調函數里判斷是否有異常發生,于是只用三次異步函數調用,就會產生下邊這種代碼。

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

可以看到,回調函數已經讓代碼變得復雜了,而異步方式下對異常的處理更加劇了代碼的復雜度。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 辽宁省| 察隅县| 庄河市| 新乐市| 商南县| 安远县| 麻阳| 萍乡市| 乃东县| 富民县| 本溪市| 石河子市| 江川县| 镇安县| 微博| 兰州市| 长汀县| 怀柔区| 成都市| 调兵山市| 台东市| 新邵县| 商南县| 同心县| 石景山区| 枞阳县| 万安县| 社旗县| 滨州市| 类乌齐县| 张掖市| 云南省| 松江区| 乾安县| 龙口市| 六枝特区| 东兴市| 芦溪县| 利津县| 如皋市| 微博|