node 事件,node的事件循环机制
InfoQ:node . js的简单解释(4):node . js的事件机制。
Node.js简单介绍(四):Node.js的事件机制
Node.js Node.js的事件机制在其Github代码库(https://github.com/joyent/node)中有一个简短的介绍:Evented I/O for V8 JavaScript。这句类似广告语的话,道出了Node.js的独到之处:基于V8引擎的事件驱动IO。在本文的这一部分,让我来揭开“Evented”这个关键词的所有谜团。
Node.js能在众多后端JavaScript技术中脱颖而出,也正是因为其基于事件的特性,才广受欢迎。对比Rhino可以看出,Rhino引擎支持的后端JavaScript无法摆脱其他语言同步执行的影响,导致后端编程与前端编程差异显著,编程模型无法统一。在前端编程中,事件被广泛使用,DOM上的各种事件。Ajax大规模应用后,异步请求得到了更广泛的认可,Ajax也是基于事件机制的。在Rhino中,文件读取等操作都是同步进行的。在这种单线程编程模式下,如果采用同步机制,则无法匹配PHP等服务器端脚本语言的成熟度,性能并不显著。直到Ryan Dahl在2009年推出Node.js,后端JavaScript才走出困惑。随着Node.js的推出,我觉得是时候改变两种情况了:
统一了前后台JavaScript的编程模型。充分利用事件机制,用异步IO突破单线程编程模型的性能瓶颈,让JavaScript在后端实现实用价值。在第二次浏览器大战的领头羊V8的及时帮助下,Node.js在短短两年内取得了可观的运营效率,并迅速被大家所接受。这从Github上Node.js项目的受欢迎程度和NPM上的库数量就可以看出来。
至于Node.js为什么选择V8 JavaScript的Evented I/O的结构和形式来实现,可以参考2011年初对作者Ryan Dahl的一次采访:http://bostinno . com/2011/01/31/node-js-interview-4-questions-with-creator-Ryan-Dahl/。
事件机制的实现Node.js中的大部分模块都继承自事件模块(http://nodejs . org/docs/latest/API/events . html)。事件模块(事件。EventEmitter)是事件侦听器模式的简单实现。具有基本事件侦听模式的方法实现,如addListener/on、once、removeListener、removeAllListeners、emit等。和前端DOM树上的事件不一样,因为它没有冒泡、逐层捕捉等属于DOM的事件的行为,也没有preventDefault()、stopPropagation()、stopImmediatePropagation()等处理事件交付的方法。
从另一个角度来看,事件侦听器模式也是事件钩子的一种机制,用于将内部数据或状态导出到外部调用方。Node.js中的很多对象都有黑盒的特点,功能点较少。如果它们不是以事件挂钩的形式出现,就无法获得对象在运行过程中的中间值或内部状态。这种方式,通过事件钩子,让程序员不用去关注组件是如何启动和执行的,而只关注需要的事件点。
var选项={
主持人: www.google.com ,
端口:80,
路径:“/上传”,
方法:“发布”
var req=http.request(options,function (res) {
console . log( STATUS: RES . STATUS code );
console . log( HEADERS: JSON . stringify(RES . HEADERS));
RES . set encoding( utf8 );
res.on(data ,function (chunk) {
console . log( BODY: chunk );
req.on(错误,函数(e) {
console.log(请求问题: e . message );
//将数据写入请求体
req . write( data n );
req . write( data n );
req . end();在这段HTTP请求的代码中,程序员只需要关注错误、数据等业务事件,不要太关注内部流程。
值得一提的是,如果您向一个事件添加超过10个侦听器,您将会得到一个警告。这样的设计和Node.js本身的单线程运行有关。设计者认为监听器太多可能导致内存泄漏,所以有这样的警告。调用:
emitter . setmaxlisteners(0);这个限制可以取消。
其次,为了提高Node.js程序的健壮性,EventEmitter对象对error事件进行了特殊处理。如果运行时的错误触发了错误事件。EventEmitter将检查是否有侦听器被添加到错误事件中。如果是,该错误将由侦听器处理;否则,该错误将作为异常抛出。如果这个异常没有被外部捕获,它将导致线程退出。
事件机制的高级应用继承事件。EventEmitter实现一个继承EventEmitter的类非常简单。以下是Node.js中继承EventEmitter的流对象的示例:
函数流(){
事件。event emitter . call(this);
util.inherits(流,事件。event emitter);Node.js将继承的方法封装在工具模块中,所以在这里可以方便地调用。程序员可以通过这种方式轻松继承EventEmitter对象。使用事件机制可以帮助您解决一些问题。
多个事件间的协作在稍微大一点的应用中,数据和Web服务器的分离是不可避免的,比如新浪微博、脸书、Twitter等。这样做的好处是数据源统一,可以针对同一个数据源开发各种富客户端程序。以Web应用为例。在呈现页面时,通常需要从多个数据源提取数据并将其呈现给客户端。在这种情况下,Node.js可以自然而方便地并行发起对多个数据源的请求。
api.getUser(用户名,函数(配置文件){
//获取了配置文件
api.getTimeline(用户名,函数(时间线){
//得到时间线
api.getSkin(用户名,函数(皮肤){
//得到了皮肤
});Node.js通过异步机制使请求畅通,达到并行请求的目的,有效调用下层资源。然而,这个场景中的问题是Node.js本身并不支持多个事件响应结果的协调。为了在进行下一步之前实现所有三个请求的结果,程序可能会被更改为以下情况:
api.getUser(用户名,函数(配置文件){
api.getTimeline(用户名,函数(时间线){
api.getSkin(用户名,函数(皮肤){
//TODO
});这将导致请求变成串行的,不能充分利用底层的API服务器。
为了解决这样的问题,我写了一个模块(https://github.com/JacksonTian/eventproxy)来实现多事件协作。以下是上述代码的改进版本:
var proxy=new event proxy();
proxy.all(profile , timeline , skin ,函数(profile,timeline,skin) {
//TODO
api.getUser(用户名,函数(配置文件){
proxy.emit(profile ,profile);
api.getTimeline(用户名,函数(时间线){
proxy.emit(timeline ,timeline);
api.getSkin(用户名,函数(皮肤){
proxy.emit(skin ,皮肤);
});EventProxy也是事件侦听器模式的一个简单实现。因为底层实现和Node.js的EventEmitter不一样,所以不能合并到Node.js中,但是提供了比EventEmitter更强大的功能,API也符合EventEmitter和Node.js的思想,可以在前端应用。
这里的all方法是指监听profile、timeline和skin方法后,执行回调函数,并传入监听接收到的数据。
最后介绍一个多事件协作的解决方案:JSCEX(https://github . com/Jeffrey Zhao/JSCEX)。Jscex通过运行时编译(或者必要时运行前编译)的思想,将同步思维的代码转换成最终的异步代码执行。写代码时可以用同步思维来写,可以享受同步思维的便捷编写和异步执行的高效性能。如果通过Jscex编写,它将采用以下形式:
var data=$await(Task.whenAll({
配置文件:api.getUser(用户名),
时间轴:api.getTimeline(username ),
skin: api.getSkin(username )
//使用data.profile,data.timeline,data.skin。
//TODO本节感谢Jscex作者@老赵(http://blog.zhaojie.me/)的指正和帮助。
利用事件队列解决雪崩问题所谓雪崩问题,就是在缓存失效的情况下,大并发、高流量会同时涌入数据库,数据库无法同时承受如此大的查询请求,从而影响网站整体的缓慢响应。那么在Node.js中如何处理这种情况呢?
var select=函数(回调){
db.select(SQL ,函数(结果){
回拨(成绩);
};以上是一个数据库查询的调用。如果站点刚刚启动,此时缓存中没有数据。但是如果访问量很大,同样的SQL会被发送到数据库重复查询,会影响服务的整体性能。一个改进是添加一个状态锁。
var status= ready
var select=函数(回调){
if(状态===就绪){
状态=“挂起”;
db.select(SQL ,函数(结果){
回拨(成绩);
状态=“就绪”;
};但是,在这个场景中,select被重复调用,只有第一次调用有效,后续的select没有数据服务。所以此时引入事件队列:
var proxy=new event proxy();
var status= ready
var select=函数(回调){
proxy.once(selected ,回调);
if(状态===就绪){
状态=“挂起”;
db.select(SQL ,函数(结果){
proxy.emit(selected ,results);
状态=“就绪”;
};在这里,EventProxy对象的once方法用于将所有请求的回调推送到事件队列中,监视器在执行一次后将被移除的特性确保了每个回调只执行一次。对于同一个SQL语句,可以保证从同一查询的开始到结束总是只有一个调用。在这个查询期间,传入的调用只需要在队列中等待数据准备好,从而节省了重复的数据库调用开销。由于Node.js的单线程执行,这里不用担心状态。其实这种方法也可以应用到其他远程调用场景中,即使没有外部缓存策略,也能有效节省重复开销。这里也可以用EventEmitter代替EventProxy,但是可能会有太多的侦听器,这可能会导致警告。您需要调用setMaxListeners(0)来移除警告或设置更大的警告阈值。
参考:3358 nodejs . org/docs/latest/API/events . html 3359 github . com/jacksonian/event proxy/blob/Master/README . MD 3359 github . com/Jeffrey Zhao/jscex/blob/Master/README-cn . MD关于作者,新浪微博@ Park Ling,前端工程师,曾就职于SAP,现就职于淘宝,花名Park Ling,致力于NodeJS和移动Web App的研发工作。双修前端JavaScript,希望把NodeJS介绍给更多的工程师。兴趣:读万卷书,行万里路。个人Github地址:http://github.com/JacksonTian.