该文章阅读需要5分钟,更多文章请点击本人博客halu886
之前总结了NodeJs中通过事件循环实现异步,包括各种基于线程池的异步I/O的API和与I/O无关的异步API。NodeJs的设计从里到外都散发着异步的气息。虽然异步为NodeJs带来了卓越的性能,但是异步编程带了部分的诋毁。
同时之前我们也总结过异步开发在应用层流行不起来的原因。逻辑上,异步编程在流程控制上,业务表达并不适合自然语言的线形思维习惯。较少人能适应异步编程,除了GUI开发者,前端开发者习以为常处理各种DOM事件和浏览器中的事件。
Javascript在浏览器中也属于事件驱动的执行过程,这使得前后端的Javascript在执行原理和风格上趋近一致,虽然执行在不同的环境,但是除了宿主环境不一样,并不能让人觉得这是一门新语言。
V8和异步带来的性能提升,前后端Javascript编程分隔一致,是Node能迅速成功并流行起来的原因。
函数式编程
在开始异步编程之前,我们先来了解一下Javascript的回调函数和深层嵌套的来龙去脉,函数(function)作为Javascript的一等公民,自由度非常高,无论是作为调用还是参数,甚至返回值均可。这是因为Javascript在诞生时借鉴了Schema语言(lisp的派生)。
高阶函数
一般来说,函数只接受一般的基本的数据类型或者对象引用,返回值也是基本数据类型和对象引用。
function foo(x){
return x;
}
高阶函数则是可以将函数作为返回值,或者将函数作为参数的函数。
function foo(){
return function(){
return x;
}
}
高阶函数看起来变化虽小。像C/C++也能通过指针实现,但是形成了一种后续传递风格(Continue Passing Style)的结果接收方式,将函数的业务重心从返回值转移到了回调函数中。
function foo(x,bar){
return bar(x);
}
以上面的高阶函数作为例子,当bar参数不同时,则返回不同结果。例如数组的sorting()
则是典型的高阶函数。
var points=[40,100,1,5,25,10];
points.sort(function(a,b){
return a - b;
})
// [1,5,10,25,40,100]
通过改动回调函数来决定业务这就是高阶函数的灵活性,同时结合Node的事件模块,事件处理方式正是基于高阶函数的特性来完成的,相同的事件注册不同的回调函数灵活的处理业务逻辑。
var emitter = new events.EventEmitter();
emitter.on('event_foo',function(){
// TODO
})
事件可以很容易的将复杂业务解耦,这都归功于高阶函数,高阶函数在Javascript中比比皆是。
偏函数用法
偏函数指的是创建一个调用一个-部分参数或变量已经预设的函数-的函数。
例如:
var toString = Object.prototype.toString;
var isString = function(obj){
return toString.call(obj) == '[object String]';
}
var isFunction = function(obj){
return toString.call(obj)=='[object Function]';
}
在Javascript中进行类型判断时,我们通常会用上述方法定义。虽然不是很复杂,只有两个函数的定义,但是存在一些重复的代码。一旦类型多起来,那么会出现更多的冗余代码。为了解决重复定义的问题,我们引入一个新函数如工厂一样批量创建类似的函数。
var isType = function(type){
return function(obj){
return toString.call(obj)=='[object ' + type + ']';
};
}
var isString = isType('String');
var isFunction = isType('Function');
可以看出,通过创建isType()
函数后,后面创建类型校验的方法就简单多了。这种通过指定部分参数产生新的定制化的函数的形式就是偏函数。
偏函数在异步编程中应用也十分广泛,著名类库Underscore提供的after()方法就是偏函数应用。
_.after = function(times,func){
if(times<=0) return func();
return function(){
if (--times < 1) {return func.apply(this,arguments);}
}
}
根据传入的times和具体方法,生成一个需要执行多次才会执行的偏函数。
异步编程的优势和难点
在单线程中,由于同步I/O耗时太久,导致I/O和CPU使用不能重叠。可是随着应用的复杂度和性能门槛提高,开发者过去通过使用多线程来提升性能,但是随之而来的上下文切换开销以及锁和同步等各种问题让开发者头痛不已。或者C/C++直接调用操作系统底层,手工实现异步I/O,但是开发和调试的门槛也随之提升。Node利用Javascript及内部异步库将异步提升应用层,这是种创新。
优势
Node的核心是基于事件驱动的非阻塞I/O模型,使得I/O和CPU并不相互依赖,让资源更好的利用。对于网络应用,并发带来的想象空间更大,延展开来的是分布式和云, 并行使得各个单点之间可以有效的连接组织起来。
传统同步模型中,分布式计算性能则会大打折扣。
Node的JavaScript线程则像是一个大管家,将任务分配给I/O线程和处理结果,I/O线程池中的I/O线程则是作为小二的角色。管家和小二则是互不相关的,这保证了整体的高效。
这个模型的缺陷则是管家无法承担过多的细节性的工作。如果承担过多,则会影响任务的调度。管家将忙个不停,而小二则得不到活干。结果是整体效率的低下。
Node是处理I/O密集型的模型,采用单线程,则使Node更像一个处理I/O密集型的能手,对于CPU密集型则看这个大管家的能耐了。
难点
Node让异步编程风靡服务器端,一方面借助异步I/O和V8引擎的高性能突破单线程性能瓶颈,另一方面统一了前后端Javascript编程模型。但是也存在很多难点。
异常处理
之前我们捕获异常通过类Java的try/catch/final语句块。
try{
JSON.parse(json);
}catch(e){
//TODO
}
但是异步I/O通常分为两步,提交请求和处理结果,两个阶段存在事件循环调度。异步方法通常在发起调用立即返回,异常不一定发生在调用的过程中,那么此时try/catch的功效在这里是不会发生任何作用。
var async = function(callback){
process.nextTick(callback);
}
调用async方法后,callback将会被存放起来,将会在下个事件循环Tick中被取出执行。尝试对异步方法进行try/catch只能捕获当此Tick循环中异常,回调中抛出的异常则无法捕获了。
try{
async(callback);
}catch(e){
//TODO
}
Node在处理异常时有一种约定俗成的习俗,将异常作为作为回调的第一个实参。如果为空,则表示没有异常。
async(function(err,results){
//TODO;
})
如果是我们自行创建的异步方法,也需要遵循以下两点
- 必须执行调用方传入的回调函数;
- 正确传递异常供调用者判断;
var async = function(callback){
process.nextTick(function(){
var results = something;
if(error){
return callback(error);
}
callback(null,result);
})
}
另一个容易犯的错误则是对用户传递的回调进行异常捕获。
try{
req.body = JSON.parse(buf,options.reviver);
callback();
}catch(err){
err.body = buf;
err.status = 400;
callback(err);
}
上述代码的意图本是捕获JSON.parse()
抛出的异常,但是也将callback()
包含进去了,一旦回调函数出现异常,那么将会被catch,则又被执行一次,将会造成业务混乱。
try{
req.body = JSON.parse(buf,options.reviver);
}catch(err){
err.body = buf;
err.status = 400;
return callback(err);
}
callback();
在编写异步方法,只需要将异常正常的传递给用户的回调即可,无需做过多的操作。
以上知识点均来自<<深入浅出Node.js>>,更多细节建议阅读书籍:-)