详解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 相关文章推荐
基于jquery的滑动样例代码
Nov 20 Javascript
《JavaScript高级编程》学习笔记之object和array引用类型
Nov 01 Javascript
javascript实现tab响应式切换特效
Jan 29 Javascript
AngularJs 指令详解及示例代码
Sep 01 Javascript
JS实现可编辑的后台管理菜单功能【附demo源码下载】
Sep 13 Javascript
vue2.0在table中实现全选和反选的示例代码
Nov 04 Javascript
VUE重点问题总结
Mar 19 Javascript
Vue filter格式化时间戳时间成标准日期格式的方法
Sep 16 Javascript
Vue 重置组件到初始状态的方法示例
Oct 10 Javascript
pageGroup.js实现分页功能
Jul 27 Javascript
layui对工具条进行选择性的显示方法
Sep 19 Javascript
详解Webpack4多页应用打包方案
Jul 16 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的ob_start来生成静态页面的方法分析
2011/03/09 PHP
php中如何使对象可以像数组一样进行foreach循环
2013/08/09 PHP
javascript编程起步(第六课)
2007/01/10 Javascript
jQuery Validation Plugin验证插件手动验证
2016/01/26 Javascript
javascript获取wx.config内部字段解决微信分享
2016/03/09 Javascript
星期几的不同脚本写法(推荐)
2016/06/01 Javascript
Validform表单验证总结篇
2016/10/31 Javascript
jQuery图片加载显示loading效果
2016/11/04 Javascript
Vue.Js中的$watch()方法总结
2017/03/23 Javascript
基于AngularJS实现的工资计算器实例
2017/06/16 Javascript
微信小程序 共用变量值的实现
2017/07/12 Javascript
vuejs 单文件组件.vue 文件的使用
2017/07/28 Javascript
mockjs,json-server一起搭建前端通用的数据模拟框架教程
2017/12/18 Javascript
React Native使用fetch实现图片上传的示例代码
2018/03/07 Javascript
Nodejs 和 Electron ubuntu下快速安装过程
2018/05/04 NodeJs
浅谈ng-zorro使用心得
2018/12/03 Javascript
vue项目刷新当前页面的三种方法
2018/12/04 Javascript
微信小程序实现两边小中间大的轮播效果的示例代码
2018/12/07 Javascript
Node.js API详解之 querystring用法实例分析
2020/04/29 Javascript
Python实现多线程抓取妹子图
2015/08/08 Python
Python模拟百度登录实例详解
2016/01/20 Python
Python常用内置模块之xml模块(详解)
2017/05/23 Python
Python的地形三维可视化Matplotlib和gdal使用实例
2017/12/09 Python
Python实现判断给定列表是否有重复元素的方法
2018/04/11 Python
python实现点对点聊天程序
2018/07/28 Python
numpy ndarray 取出满足特定条件的某些行实例
2019/12/05 Python
Python lambda表达式原理及用法解析
2020/08/18 Python
Python利用matplotlib绘制散点图的新手教程
2020/11/05 Python
详解python的xlwings库读写excel操作总结
2021/02/26 Python
成功经营餐厅的创业计划书范文
2013/12/26 职场文书
初一生物教学反思
2014/01/18 职场文书
网络研修随笔感言
2014/02/17 职场文书
《会走路的树》教后反思
2014/04/19 职场文书
2016领导干部廉洁从政心得体会
2016/01/19 职场文书
DjangoRestFramework 使用 simpleJWT 登陆认证完整记录
2021/06/22 Python
了解MySQL查询语句执行过程(5大组件)
2022/08/14 MySQL