上篇文章我們講完了類和對象,接下來我們將要說回調函數.
我在第一篇說到nodejs的一個優勢是異步IO,實際上異步IO直接體現就是使用回調函數,當然不是用了回調函數,他就一定是異步IO的,因為inodejs是一個單線程函數,主線程在執行的時候,只有發生了異步處理(文件讀寫、網絡請求、定時任務、讀取數據庫等),js讓操作系統相關部件去處理這些請求,另一方面,它會繼續執行后面的代碼,這才是異步。
回調函數在完成任務后就會被調用,很多Node項目使用了大量的回調函數,Node 所有 API 都支持回調函數。
例如,我們可以一邊處理某一個復雜邏輯運算,一邊執行其他命令,在復雜邏輯運算完成后,我們將運算結果作為回調函數的參數返回。這樣在執行代碼時就沒有阻塞或等待IO操作。這就大大提高了 Node.js 的性能,可以處理大量的并發請求。
回調函數一般作為函數的最后一個參數出現:
function fun1(param1, param2, callback) { }
function fun2(param, callback1, callback2) { }
阻塞IO代碼
代碼如下:
var fs=require("fs");
//demo.txt文件內容是 hello world
var data=fs.readFileSync("demo.txt");
console.log(data.toString());
console.log("讀文件結束");
var a = 12;
console.log("執行其他操作結束");
以上代碼執行結果如下:
hello world
讀文件結束
執行其他操作結束
非阻塞IO代碼
我們把剛才的代碼做個改動
const fs = require('fs')
//demo.txt文件內容是 hello world
fs.readFile('demo.txt', 'utf8', function(err, data){console.log(data);console.log("讀文件結束");
});
var a = 12;
console.log("執行其他操作結束");
?以上代碼執行結果如下:
執行其他操作結束
hello world
讀文件結束
?
以上兩個實例我們了解了阻塞與非阻塞調用的不同。第一個實例在文件讀取完后才執行程序。 第二個實例我們不需要等待文件讀取完,這樣就可以在讀取文件時同時執行接下來的代碼,大大提高了程序的性能。
因此,阻塞是按順序執行的,而非阻塞是不需要按順序的,所以如果需要處理回調函數的參數,我們就需要寫在回調函數內。
異常處理
JS 自身提供的異常捕獲和處理機制—try catch,只能用于同步執行的代碼。以下是一個例子。
try {var b = a /0;
} catch (err) {console.log('Error: %s', err.message);
}
輸出結果為:
Error: a is not defined
可以看到,異常會沿著代碼執行路徑一直順序執行,直到遇到第一個 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);
}-- Console ------------------------------
/home/user/test.js:4callback(fn());^
TypeError: object is not a functionat 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.}
});-- Console ------------------------------
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 somethingasyncB(function (err, data) {if (err) {callback(err);} else {// Do somethingasyncC(function (err, data) {if (err) {callback(err);} else {// Do somethingcallback(null);}});}});}});
}main(function (err) {if (err) {// Deal with exception.}
});
可以看到,回調函數已經讓代碼變得復雜了,而異步方式下對異常的處理更加劇了代碼的復雜度。如果 NodeJS 的最大賣點最后變成這個樣子,那就沒人愿意用 NodeJS 了。