上篇文章我们讲完了类和对象,接下来我们将要说回调函数.
我在第一篇说到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 了。