回调
- 异步编程依托于回调来实现,但不能说使用了回调后程序就异步化了
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');
-- Console ------------------------------
100000000
hello
- 以上代码中的回调函数仍然先于后续代码执行。JS本身是单线程运行的,不可能在一段代码还未结束运行时去运行别的代码,因此也就不存在异步执行的概念。
- 如果某个函数做的事情是创建一个别的线程或进程,并与JS主线程并行地做一些事情,并在事情做完后通知JS主线程,那情况又不一样了
setTimeout(function () {
console.log('world');
}, 1000);
console.log('hello');
-- Console ------------------------------
hello
world
- 我们可以认为
setTimeout
这类JS规范之外的由运行环境提供的特殊函数做的事情是创建一个平行线程后立即返回,让JS主进程可以接着执行后续代码,并在收到平行进程的通知后再执行回调函数 - 除了
setTimeout
、setInterval
这些常见的,这类函数还包括NodeJS提供的诸如fs.readFile
之类的异步API。 - JS是单线程运行的这个事实上,这决定了JS在执行完一段代码之前无法执行包括回调函数在内的别的代码。也就是说,即使平行线程完成工作了,通知JS主线程执行回调函数了,回调函数也要等到JS主线程空闲时才能开始执行
代码设计模式
函数返回值
- 使用一个函数的输出作为另一个函数的输入是很常见的需求,在同步方式下一般按以下方式编写代码:
var output = fn1(fn2('input'));
- 在异步方式下,由于函数执行结果不是通过返回值,而是通过回调函数传递,因此一般按以下方式编写代码:
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.
}));
- 与异步串行遍历的版本相比,以上代码并行处理所有数组成员,并通过计数器变量来判断什么时候所有数组成员都处理完毕了。
异常处理
-
S自身提供的异常捕获和处理机制——try..catch..,只能用于同步执行的代码。
-
异常会沿着代码执行路径一直冒泡,直到遇到第一个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: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.
}
});
-- 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 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.
}
});
域(Domain)
- NodeJS提供了
domain
模块,可以简化异步代码的异常处理 - 简单的讲,一个域就是一个JS运行环境,在一个运行环境中,如果一个异常没有被捕获,将作为一个全局异常被抛出。
- NodeJS通过
process
对象提供了捕获全局异常的方法
process.on('uncaughtException', function (err) {
console.log('Error: %s', err.message);
});
setTimeout(function (fn) {
fn();
});
-- Console ------------------------------
Error: undefined is not a function
- 虽然全局异常有个地方可以捕获了,但是对于大多数异常,希望尽早捕获,并根据结果决定代码的执行路径 , 以下HTTP服务器代码作为例子:
function async(request, callback) {
// Do something.
asyncA(request, function (err, data) {
if (err) {
callback(err);
} else {
// Do something
asyncB(request, function (err, data) {
if (err) {
callback(err);
} else {
// Do something
asyncC(request, function (err, data) {
if (err) {
callback(err);
} else {
// Do something
callback(null, data);
}
});
}
});
}
});
}
http.createServer(function (request, response) {
async(request, function (err, data) {
if (err) {
response.writeHead(500);
response.end();
} else {
response.writeHead(200);
response.end(data);
}
});
});
- 以上代码将请求对象交给异步函数处理后,再根据处理结果返回响应。
- 这里采用了使用回调函数传递异常的方案,因此async函数内部如果再多几个异步函数调用的话,代码就变成上边这副鬼样子了。
- 为了让代码好看点,可以在每处理一个请求时,使用
domain
模块创建一个子域(JS子运行环境)。 - 在子域内运行的代码可以随意抛出异常,而这些异常可以通过子域对象的error事件统一捕获。于是以上代码可以做如下改造:
function async(request, callback) {
// Do something.
asyncA(request, function (data) {
// Do something
asyncB(request, function (data) {
// Do something
asyncC(request, function (data) {
// Do something
callback(data);
});
});
});
}
http.createServer(function (request, response) {
var d = domain.create();
d.on('error', function () {
response.writeHead(500);
response.end();
});
d.run(function () {
async(request, function (data) {
response.writeHead(200);
response.end(data);
});
});
});
- 使用
.create
方法创建了一个子域对象,并通过.run
方法进入需要在子域中运行的代码的入口点。而位于子域中的异步函数回调函数由于不再需要捕获异常
陷阱
- 无论是通过
process
对象的uncaughtException事件捕获到全局异常,还是通过子域对象的error事件捕获到了子域异常,在NodeJS官方文档里都强烈建议处理完异常后立即重启程序,而不是让程序继续运行。 - 按照官方文档的说法,发生异常后的程序处于一个不确定的运行状态,如果不立即退出的话,程序可能会发生严重内存泄漏,也可能表现得很奇怪。
- 这里需要澄清一些事实。JS本身的
throw..try..catch
异常处理机制并不会导致内存泄漏,也不会让程序的执行结果出乎意料,但NodeJS并不是存粹的JS。NodeJS里大量的API内部是用C/C++实现的,因此NodeJS程序的运行过程中,代码执行路径穿梭于JS引擎内部和外部,而JS的异常抛出机制可能会打断正常的代码执行流程,导致C/C++部分的代码表现异常,进而导致内存泄漏等问题。 - 因此,使用uncaughtException或domain捕获异常,代码执行路径里涉及到了C/C++部分的代码时,如果不能确定是否会导致内存泄漏等问题,最好在处理完异常后重启程序比较妥当。而使用try语句捕获异常时一般捕获到的都是JS本身的异常,不用担心上诉问题。