详解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 相关文章推荐
javascript Xml增删改查(IE下)操作实现代码
Jan 30 Javascript
jQuery jqgrid 对含特殊字符json 数据的 Java 处理方法
Jan 01 Javascript
jQuery获取Select选择的Text和Value(详细汇总)
Jan 25 Javascript
javascript:;与javascript:void(0)使用介绍
Jun 05 Javascript
jquery ajax方式直接提交整个表单核心代码
Aug 15 Javascript
JS如何将数字类型转化为没3个一个逗号的金钱格式
Jan 27 Javascript
javascript学习笔记(二)数组和对象部分
Sep 30 Javascript
Bootstrap网格系统详解
Apr 26 Javascript
jQuery 3.0十大新特性最终版发布
Jul 14 Javascript
基于JavaScript实现移动端无限加载分页
Mar 27 Javascript
ES6新特性之Symbol类型用法分析
Mar 31 Javascript
vue 实现弹窗关闭后刷新效果
Apr 08 Vue.js
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之第六天
2006/10/09 PHP
PHP操作XML作为数据库的类
2010/12/19 PHP
PHP中浮点数计算比较及取整不准确的解决方法
2015/01/09 PHP
PHP标准类(stdclass)用法示例
2016/09/28 PHP
JavaScript 面向对象之命名空间
2010/05/04 Javascript
JS 获取鼠标左右键的键值方法
2014/10/11 Javascript
javascript异步编程代码书写规范Promise学习笔记
2015/02/11 Javascript
JavaScript生成的动态下雨背景效果实现方法
2015/02/25 Javascript
Javascript实现Array和String互转换的方法
2015/12/21 Javascript
使用jQuery或者原生js实现鼠标滚动加载页面新数据
2016/03/06 Javascript
Vue 仿百度搜索功能实现代码
2017/02/16 Javascript
JS 验证码功能的三种实现方式
2018/11/26 Javascript
Node.js系列之发起get/post请求(2)
2019/08/30 Javascript
原生js+ajax分页组件
2020/01/30 Javascript
[01:08:29]DOTA2-DPC中国联赛定级赛 RNG vs Aster BO3第一场 1月9日
2021/03/11 DOTA
[54:05]DOTA2-DPC中国联赛定级赛 SAG vs iG BO3第一场 1月9日
2021/03/11 DOTA
Python threading多线程编程实例
2014/09/18 Python
Python编写电话薄实现增删改查功能
2016/05/07 Python
python实现解数独程序代码
2017/04/12 Python
Python虚拟环境项目实例
2017/11/20 Python
Python爬虫框架Scrapy常用命令总结
2018/07/26 Python
python 发送和接收ActiveMQ消息的实例
2019/01/30 Python
24式加速你的Python(小结)
2019/06/13 Python
Django1.11自带分页器paginator的使用方法
2019/10/31 Python
使用Python脚本从文件读取数据代码实例
2020/01/19 Python
CSS3实现div从下往上滑入滑出效果示例
2020/04/28 HTML / CSS
CSS3中animation实现流光按钮效果
2020/12/21 HTML / CSS
前端canvas动画如何转成mp4视频的方法
2019/06/17 HTML / CSS
缅甸网上购物:Shop.com.mm
2017/12/05 全球购物
电大学习个人自我评价范文
2013/10/04 职场文书
金融专业大学生职业生涯规划范文
2014/01/16 职场文书
批评与自我批评材料
2014/02/15 职场文书
合伙开公司协议书范本
2014/10/28 职场文书
导盲犬小Q观后感
2015/06/11 职场文书
SpringBoot+VUE实现数据表格的实战
2021/08/02 Java/Android
python中Pyqt5使用Qlabel标签播放视频
2022/04/22 Python