该文章阅读需要5分钟,更多文章请点击本人博客halu886
异步编程的难点
函数嵌套过深
这应该是Node中最受人诟病的地方。在前端开发中,较少存在异步多级依赖的业务场景。
$(selector).click(function(event){
// TODO
});
$(selector).change(function(event){
// TODO
})
但是在Node中,事物中多级异步调用的场景比比皆是。
fs.readdir(path.join(_dirname,'..'),function(err,files){
files.forEach(function(filename,index){
fs.readFile(filename,'utf8',funtion(err,file){
// TODO
})
})
})
在上述场景中,因为两次操作存在依赖关系,嵌套情有可原。但是在网页渲染中,通常需要数据,模板,静态文件,但是三者并不相互依赖,但是最终渲染结果三者缺一不可,如果采用默认的异步方法调用。
fs.readFile(template_path,'utf8',function(err,template){
db.query(sql,function(err,data){
l10n.get(function(err,resources){
// TODO
})
})
})
虽然从结果上来说这是没有问题的,但是这并没有使用Node的并行优势。
阻塞代码
对于Javascript的开发者可能会困惑,如何实现沉睡线程的功能,setTimeout()
和setInterval()
能延后操作,但是不能阻塞后面的代码执行。
所以,我们可能会这样实现sleep(1000)
的效果。
// TODO
var start = new Date();
while(new Date() - start <1000){
// TODO
}
// 需要阻塞的代码
但是事实却是,CPU会持续计算,根本就没有起到线程沉睡的功能,并且Node是单线程的,CPU所有的资源都在为这段代码服务,导致任何请求都得不到响应。
存在这种需求,统一规划业务逻辑,调用setTimeout()实现效果会更好。
多线程编程
我们在讨论Javascript编程时,通常都是单线程编程,前端中UI渲染和Javascript执行线程共用一个线程。在Node中,只是没有UI渲染,模型基本相同。但是在多核服务器中,单个Node进程实际上没有充分利用多核CPU。随着业务复杂化,对于多核CPU的要求也会越来越高。浏览器提出Web Workers。它通常将Javascript执行与UI渲染分离,可以通过多核CPU进行大量运算。并且Web Worker也是一个通过消息机制合理使用多核CPU的合理模型。
遗憾的是浏览器对于标准存在明显的滞后,导致Web Worker并没有广泛的应用。并且虽然Web Worker解决了多核CPU和渲染UI的问题,但是并没有解决UI渲染的效率问题。但是Node借鉴了Web Worker的模式,child_process是基础API,cluster则是它的深层次应用。
异步编程解决方案
以上我们列举了一下异步编程的缺点,和异步编程的高性能相比,编程过程看起来并没有那么完美。但是事实也并没有那么糟糕,与问题相比,解决方案总是更多。
- 事件发布/订阅模式
- Promise/Deferred模式
- 流程控制库
事件发布/订阅模式
事件监听器模式是广泛用于异步编程的模式,将回调函数异步化。又称发布/订阅模式
Node自身的events模块是发布订阅的一个简单实现,Node大多数模块都继承自它,这比前端的事件机制简单的多,不存在事件冒泡,也不存在preventDefault()
,stopPropagetion()
,stopImmediatePropagation()
等控制事件传递的方法。具有addListener/on()
,once()
,removeListener()
,removeAllListeners()
和emit()
等基本的事件监听模式的方法实现。
emitter.on("event1",function(message){
console.log(message);
})
emitter.emit('event1',"I am message!");
订阅事件是高阶函数的应用,一个事件能够与多个回调函数相关联,一个回调函数又称为事件监听器。当使用emit()
发布事件后,消息会传递给注册的事件监听器都会被执行,并且监听器能够方便的添加或者删除,这样能够实现事件和具体逻辑的解耦。
事件发布/订阅自身没有同步和异步的概念,emit()
基于事件循环的概念而异步触发的。那么可以理解为发布/订阅模式应用于异步编程。
事件发布/订阅模式模式主要用于业务的解耦,事件的发布者不用关心订阅的监听者的业务逻辑是什么,有多少个监听者,数据可以通过消息的方法灵活的流转。可以将一个流程中不变的封装成一个个组件,容易变化的暴露出去给外部处理,可以理解为事件的设计是组件的接口设计。
从另外一个角度上来看,事件监听也是一种钩子(hook)模式,Node中大多数对象都是黑盒,可以通过事件将对象在运行状态的状态通过事件传递出来。
var options = {
host: 'www.google.com',
port: 80,
path: '/upload',
method: 'POST'
}
var req = http.request(options,function(res){
console.log('STATUS' + res.statusCode);
console.log('HEADERS:' + JSON.stringify(res.headers));
res.setEncoding('utf8');
res.on('data',function(chunk){
console.log('BODY:'+chunk);
});
res.on('end',function(){
// TODO
})
req.on('error',function(e){
console.log('problem with request:' + e.message);
})
req.write('data\n');
req.write('data\n');
req.end();
})
在这段http请求中,我们只需要将重点放在error,data,end事件上,业务流程则不需要过于关注。
下面有两个基于健壮性考虑的细节
- 如果事件的监听器超过10个,将会获得一条警告,初衷是担心导致内存泄露。可以通过emitter.setMaxListeners(0)关闭这个限制。
- 为了处理异常,当事件处理中发生了一个异常,实例会将这个异常传递给已经存在error监听者,如果不存在异常监听者进行捕获,这个异常将会向外抛出,最后如果没有被捕获,则会导致线程推出,一个健壮的EventEmitter应该有异常监听者。
集成events模块
实现继承一个EventEmitter类也很简单
var events = require('events');
function Stream(){
events.EventEmitter.call(this);
}
util.inherits(Stream,events.EventEmitter);
使用util轻松的继承的EventEmitter事件,通过事件来解决业务问题,Node中核心模块有一半的对象继承了EventEmitter对象。
利用事件队列解决雪崩问题
在事件订阅/发布模式中,存在once()
方法,事件和监听器关联,只会被执行一次,之后就会解除关联。这个可以帮助我们过滤掉一些重复性的事件响应。
在计算机中,将缓存存储在内存中加快数据的读取。雪崩问题指的是当高访问量时,大并发量的情况下,缓存失效。此时大量请求涌入数据库,导致网站整体的性能。
var select = function(callback){
db.select("SQL",function(results){
callback(results);
})
}
如果站点刚好启动,此时缓存不存在数据,但是如果访问量巨大时,同一条sql会在数据库中反复查询,会影响服务的整体性能。
可以添加一个状态锁。
var status = "ready";
var select = function(callback){
if(status === "ready"){
status = "pending";
db.select("SQL",function(results){
status = "ready";
callback(results);
})
}
}
此时连续调用多次,只有第一条SQL执行成功,后续的SQL则是失效的。
这个时候可以引入队列服务。
var proxy = new events.EventEmitter();
var status = "ready";
var select = function(callback){
proxy.once("selected",callback);
if(status === "ready"){
status = "pending";
db.select("SQL",function(results){
proxy.emit("selected",results);
status = "ready";
})
}
}
这里我们利用了once()
的特性,将所有的回调都压入事件队列中。在相同的SQL完成时,将得到的结果被所有的回调共同调用,所有回调都只会运行一次,执行完后就会被销毁。并且由于Node单线程的原因,也不用担心数据同步的问题。这个也能应用到其他远程调用的场景,即使外部没有缓存策略,也能节省重复开销。
不过此处可能会因为监听器过多而产生警告,需要调用setMaxListeners(0)
移除掉警告,或者设置更大的警告阙值。
以上知识点均来自<<深入浅出Node.js>>,更多细节建议阅读书籍:-)
楼主的坚持让我佩服,不要灰心,写文章大部分时候不是为了给别人看而是给自己看的,万事开头难,加油。
加油!
谢谢楼上大佬们的鼓励:)