Node
借助异步I/O
模型及V8
高性能引擎,突破单线程的性能瓶颈,让JavaScript
在后端达到实用价值,一方面也统一了JavaScript
的编程模型。
异步编程的难点
1. 异常处理
通常我们处理异常时,使用try/catch/final语句块进行异常捕获:
try{
JSON.parse(json)
}catch(e){
// TODO
}
但是对于异步编程而言并不一定适用,因为异步I/O的实现主要包含两个阶段:提交请求和处理结果。这两个阶段中间有事件循环的调度,两者彼此不关联。异步方法则通常在第一个阶段是提交请求后立即返回,因为异常并不一定发生在这个阶段。
try{
async(callback)
}catch(err){
//TODO
}
在调用异步方法执行后,
callback
被存放起来,知道下一个事件循环(Tick
)才会取出来执行,所以尝试对异步方法进行try/catch操作只能不做档次事件循环内的异常,对callback执行抛出的异常将无能为力。
Node在处理异常上形成了一种约定,将异常作为回调函数的第一个实参传回,如果为空值则表明没有异常抛出:
async(function(err,results){
//TODO
});
我们自行编写的异常方法上,也需要遵循这样一些原则:
原则一: 必须执行调用者传入的回调函数;
原则二: 正确传递回异常提供调用者判断;
示例如下:
var async = function (callback) {
process.nextTick(function () {
var results = something;
if (error) {
return callback(error)
}
callback(null, results)
});
};
在异步的方法编写中,另一个容易犯的错误是对用户传递的回电函数进行异常捕获,如下:
try {
req.body = JSON.parse(buf, option.reviver);
callback();
} catch (err) {
err.body = buf;
err.status = 400;
callback(err);
}
上面代码中,本意是捕获 JSON.parse
中可能出现的异常,但是当callback()
抛出异常的时候,意味着进入catch
中执行,callback()
被执行了两次。我们可以把代码改成如下:
try {
req.body = JSON.parse(buf, option.reviver);
} catch (err) {
err.body = buf;
err.status = 400;
return callback(err);
}
callback();
编写异步方法的时候,我们只需要将异常正确的传递给用户的回调方法即可,无需过多的处理。
2. 函数嵌套过深(回调地狱)
应该是node被人诟病最多的地方,事务中存在多个异步调用的场景比比皆是。
fs.readdir(path.join(_dirname, '..'), function (err, files) {
files.forEach(function (filename, index) {
fs.readfile(filename, 'utf8', function (err, file) {
//TODO
});
});
});
上面的两次操作存在依赖关系,函数嵌套的行为情有可原,这也是异步的优势所在。且看后面怎么解决。
3. 阻塞代码
对于JavaScript
而言,没有sleep()
这样的线程沉睡功能,唯独能用于演示操作的只有setInterval()
和setimeout()
这两个函数。然人惊讶的是,这两个函数并不能阻塞后续代码的持续执行。所以很多开发者会写出下属这样的代码来实现sleep(1000)的效果:
var start = new Date();
while(new Date() - start < 1000){
//TODO
}
// 需要阻塞的代码
这段代码会持续占用CPU进行判断,预期真正的线程沉睡相去甚远,破坏了事件循环的调度。由于node单线程的原因,CPU资源全都会用于为这段代码服务,导致其与任何请求都会得不到响应。
4. 多线程编程
我们往往聊JavaScript
是单一线程上执行的代码,这是在浏览器中指的是JavaScript
执行线程与UI
渲染公用的一个线程;在node中,只是没有U
I渲染的部分,模型基本相同。
5. 异步转同步
Node
上绝大部分的异步API
和少量的同步API
,偶尔出现的同步需求将会因为没有同步API让我们无所适从。但良好的流程控制,还是能够将逻辑梳理成顺序式的形式。