详解Node.js中的事件机制


Posted in Javascript onSeptember 22, 2016

前言

在前端编程中,事件的应用十分广泛,DOM上的各种事件。在Ajax大规模应用之后,异步请求更得到广泛的认同,而Ajax亦是基于事件机制的。

通常js给我们的第一印象就是运行在客户端浏览器上面的脚本,通过node.js我们可以在服务端运行javascript.

node.js是基于单线程无阻塞异步式的I/O,异步式的I/O指的是当遇到I/O操作的时候,线程不阻塞而是进行下面的操作,那么I/O操作完成之后,线程时如何知道该操作完成的呢?

当操作完成耗时的I/O操作之后,会以事件的形式通知I/O操作的线程完成,线程会在特定的时候来处理这个事件,进行下一步的操作,为了完成异步I/O,线程必须有事件循环的机制,不停的坚持是否有没有完成的事件,依次完成这些事件的处理。

而对于阻塞式I/O,线程遇到耗时的I/O操作会停止继续执行,等待操作的完成,这个时候线程就不能接受其他的操作请求,为了提供吞吐量,必须创建多个线程,每个线程去响应一个客户的请求,但是同一时间,一个cpu核心上面只能运行一个线程,多个线程要想执行就必须在不同的线程之间进行切换。

因此node.js少了多线程中线程的创建,以及线程的切换的开销,线程切换的代价是非常大的,需要为其分配内存,列入调度,同时在线程切换的时候需要执行内存换页等等操作,采用单线程的方式就可以减少这些操作。但是这种编程方式也有缺点,不符合人们的设计思维。

node.js是基于事件的模式来实现异步I/O的,当其启动之后会不停的遍历是否有为完成的事件,然后进行执行,执行完成之后会以另外一个事件的形式通知线程,本操作已经完成,这个事件又会被添加到未完成的事件列表中,线程在接下来的某个时刻遍历到这个事件然后进行执行,在这种机制中,需要将一个大的任务分成一个个小的事件,node.js也适合处理一些高I/O,低逻辑的场景。

下面的例子演示异步的文件读取:

var fs = require('fs'); 
fs.readFile('file.txt', 'utf-8', function(err, data) { 
if (err) { 
<span style="white-space:pre"> </span>console.error(err); 
} else { 
<span style="white-space:pre"> </span>console.log(data); 
} 
}); 
[javascript] view plain copy
console.log("end");

如上fs.readFile异步读取文件,之后流程就会继续走,并不会等待其读取完文件,当文件读取完毕之后,会发布一个事件,执行线程遍历到该事件就会去执行对应的操作,这里是执行相应的回调函数,例子中字符串end会比文件内容先打印出来。

node.js的事件API

events.EventEmitter:EventEmitter对node.js中的事件发射与事件监听功能提供了封装,每个事件由一个标识事件名的字符串和对应的操作组成。

事件的监听:

var events = require("events"); 
var emitter = new events.EventEmitter(); 
 <span style="font-family: Arial, Helvetica, sans-serif;">emitter.on("eventName", function(){</span> 
  console.log("eventName事件发生") 
})

事件的发布:

emitter.emit("eventName");

发布事件的时候我们可以传入多个参数,第一个参数表示事件的名称,其后的参数表示传入的参数,这些参数会被传入到事件的回调函数中。

EventEmitter.once("eventName", listener) :为事件注册一个只执行一次的监听器,当事件第一次发生并触发监听器之后,该监听器就会解除,之后如果事件发生,该监听器不会执行。

EventEmitter.removeListener(event, listener) :移除掉事件的监听器

EventEmitter.removeAllListeners(event) :移除掉事件的所有的监听器

EventEmitter.setMaxListeners(n) :node.js默认单个事件最大的监听器个数是10,如果超过10会给予警告,这么做是为了防止内存的溢出,我们可以更改这种限制设置为其他的数字,如果设置为0表示不进行限制。

EventEmitter.listeners(event) :返回某个事件的监听器列表

多事件之间协作
在略微大一点的应用中,数据与Web服务器之间的分离是必然的,如新浪微博、Facebook、Twitter等。这样的优势在于数据源统一,并且可以为相同数据源制定各种丰富的客户端程序。

以Web应用为例,在渲染一张页面的时候,通常需要从多个数据源拉取数据,并最终渲染至客户端。Node.js在这种场景中可以很自然很方便的同时并行发起对多个数据源的请求。

api.getUser("username", function (profile) {
 // Got the profile
});
api.getTimeline("username", function (timeline) {
 // Got the timeline
});
api.getSkin("username", function (skin) {
 // Got the skin
});

Node.js通过异步机制使请求之间无阻塞,达到并行请求的目的,有效的调用下层资源。但是,这个场景中的问题是对于多个事件响应结果的协调并非被Node.js原生优雅地支持。

为了达到三个请求都得到结果后才进行下一个步骤,程序也许会被变成以下情况:

api.getUser("username", function (profile) {
 api.getTimeline("username", function (timeline) {
  api.getSkin("username", function (skin) {
   // TODO
  });
 });
});

这将导致请求变为串行进行,无法最大化利用底层的API服务器。

为解决这类问题,我曾写作一个模块来实现多事件协作,以下为上面代码的改进版:

var proxy = new EventProxy();
proxy.all("profile", "timeline", "skin", function (profile, timeline, skin) {
 // TODO
});
api.getUser("username", function (profile) {
 proxy.emit("profile", profile);
});
api.getTimeline("username", function (timeline) {
 proxy.emit("timeline", timeline);
});
api.getSkin("username", function (skin) {
 proxy.emit("skin", skin);
});

EventProxy也是一个简单的事件侦听者模式的实现,由于底层实现跟Node.js的EventEmitter不同,无法合并进Node.js中。但是却提供了比EventEmitter更强大的功能,且API保持与EventEmitter一致,与Node.js的思路保持契合,并可以适用在前端中。
这里的all方法是指侦听完profile、timeline、skin三个方法后,执行回调函数,并将侦听接收到的数据传入。

最后还介绍一种解决多事件协作的方案,通过运行时编译的思路(需要时也可在运行前编译),将同步思维的代码转换为最终异步的代码来执行,可以在编写代码的时候通过同步思维来写,可以享受到同步思维的便利写作,异步执行的高效性能。

如果通过Jscex编写,将会是以下形式:

var data = $await(Task.whenAll({
 profile: api.getUser("username"),
 timeline: api.getTimeline("username"),
 skin: api.getSkin("username")
}));
// 使用data.profile, data.timeline, data.skin
// TODO

利用事件队列解决雪崩问题

所谓雪崩问题,是在缓存失效的情景下,大并发高访问量同时涌入数据库中查询,数据库无法同时承受如此大的查询请求,进而往前影响到网站整体响应缓慢。

那么在Node.js中如何应付这种情景呢。

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) {
    callback(results);
    status = "ready";
   });
  }
 };

但是这种情景,连续的多次调用select发,只有第一次调用是生效的,后续的select是没有数据服务的。所以这个时候引入事件队列吧:

var proxy = new EventProxy();
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";
   });
  }
 };

这里利用了EventProxy对象的once方法,将所有请求的回调都压入事件队列中,并利用其执行一次就会将监视器移除的特点,保证每一个回调只会被执行一次。对于相同的SQL语句,保证在同一个查询开始到结束的时间中永远只有一次,在这查询期间到来的调用,只需在队列中等待数据就绪即可,节省了重复的数据库调用开销。由于Node.js单线程执行的原因,此处无需担心状态问题。这种方式其实也可以应用到其他远程调用的场景中,即使外部没有缓存策略,也能有效节省重复开销。此处也可以用EventEmitter替代EventProxy,不过可能存在侦听器过多,引发警告,需要调用setMaxListeners(0)移除掉警告,或者设更大的警告阀值。

总结

以上就是关于Node.js中事件机制的全部内容,希望这篇文章对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。

Javascript 相关文章推荐
神奇的代码 通杀各种网站-可随意修改复制页面内容
Jul 17 Javascript
jQuery Ajax 实例全解析
Apr 20 Javascript
Javascript的常规数组和关联数组对比小结
May 24 Javascript
jQuery插件uploadify实现ajax效果的图片上传
Jun 18 Javascript
Angular和百度地图的结合实例代码
Oct 19 Javascript
js中作用域的实例解析
Mar 16 Javascript
JS给按钮添加跳转功能类似a标签
May 30 Javascript
Angular.js中window.onload(),$(document).ready()的写法浅析
Sep 28 Javascript
Vue中的验证登录状态的实现方法
Mar 09 Javascript
JS实现可视化音频效果的实例代码
Jan 16 Javascript
vue路由权限校验功能的实现代码
Jun 07 Javascript
javascript实现文字跑马灯效果
Jun 18 Javascript
AngularJS通过$sce输出html的方法
Sep 22 #Javascript
JavaScript 随机验证码的生成实例代码
Sep 22 #Javascript
D3.js实现雷达图的方法详解
Sep 22 #Javascript
javascript函数中的3个高级技巧
Sep 22 #Javascript
JavaScript省市区三级联动菜单效果
Sep 21 #Javascript
Angular2 环境配置详细介绍
Sep 21 #Javascript
JS实现鼠标滑过显示边框的菜单效果
Sep 21 #Javascript
You might like
如何把PHP转成EXE文件
2006/10/09 PHP
PHP与MySQL开发的8个技巧小结
2010/12/17 PHP
深入PHP curl参数的详解
2013/06/17 PHP
浅析THINKPHP的addAll支持的最大数据量
2015/02/03 PHP
php实现连接access数据库并转txt写入的方法
2017/02/08 PHP
Laravel框架中Blade模板的用法示例
2017/08/30 PHP
PHP后期静态绑定之self::限制实例分析
2018/12/21 PHP
jQuery 选择表格(table)里的行和列及改变简单样式
2012/12/15 Javascript
JS将光标聚焦在文本最后的实现代码
2014/03/28 Javascript
Jquery插件easyUi实现表单验证示例
2015/12/15 Javascript
jQuery插件cxSelect多级联动下拉菜单实例解析
2016/06/24 Javascript
JavaScript探测CSS动画是否已经完成的方法
2016/08/30 Javascript
纯JS打造网页中checkbox和radio的美化效果
2016/10/13 Javascript
浅谈JS函数定义方式的区别
2016/10/30 Javascript
原生JS实现垂直手风琴效果
2017/02/19 Javascript
微信小程序实现倒计时60s获取验证码
2020/04/17 Javascript
详解vue指令与$nextTick 操作DOM的不同之处
2018/08/02 Javascript
jQuery动态操作表单示例【基于table表格】
2018/12/06 jQuery
Django 外键的使用方法详解
2019/07/19 Python
详解Django模版中加载静态文件配置方法
2019/07/21 Python
树莓派3 搭建 django 服务器的实例
2019/08/29 Python
基于python实现计算且附带进度条代码实例
2020/03/31 Python
Python如何使用27行代码绘制星星图
2020/07/20 Python
python实现数学模型(插值、拟合和微分方程)
2020/11/13 Python
MANGO官方网站:西班牙芒果服装品牌
2017/01/15 全球购物
迷你唐卡软皮鞋:Minnetonka Moccasin
2018/05/01 全球购物
英国浴室洗脸盆购物网站:Click Basin
2018/06/08 全球购物
什么是静态路由?什么是动态路由?各自的特点是什么?
2015/09/16 面试题
高级人员简历的自我评价分享
2013/11/03 职场文书
2014年计划生育工作总结
2014/11/14 职场文书
大学感恩节活动总结
2015/05/05 职场文书
叶问观后感
2015/06/15 职场文书
如何制定一份可行的计划!
2019/06/21 职场文书
MySQL数据库必备之条件查询语句
2021/10/15 MySQL
MySQL实现配置主从复制项目实践
2022/03/31 MySQL
MySQL常用慢查询分析工具详解
2022/08/14 MySQL