详解Node.js中的事件机制
作者:yangnianbing110 发布时间:2024-05-03 15:58:52
前言
在前端编程中,事件的应用十分广泛,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)
移除掉警告,或者设更大的警告阀值。
总结


猜你喜欢
- 1.简介map 是 Golang 中的方便而强大的内建数据结构,是一个同种类型元素的无序组,元素通过另一类型唯一的键进行索引。其键可以是任何
- 本文研究的主要是Python程序运行原理,具体介绍如下。编译型语言(C语言为例)动态型语言一个程序是如何运行起来的?比如下面的代码#othe
- 实际线上的场景比较复杂,当时涉及了truncate, delete 两个操作,经确认丢数据差不多7万多行,等停下来时,差不多又有共计1万多行
- 如下所示:from ctypes import *import osimport win32con,win32clipboardaStrin
- 一、Python2中的字符存在的解码编码问题如果是现在正在用Python2的人应该都知道存在字符编码问题,就举一个最简单的例子吧:Pytho
- IE6绝对定位的bug及其解决办法。position:absolute定位在IE6下存在left和bottom的定位错误问题:<!–I
- 目录一、时间序列数据的生成二、Pandas设置索引三、 时间序列数据的截取四、Pandas重复值处理4.1 查询是否有重复值4.2 去除重复
- React 是 Facebook 里一群牛 X 的码农折腾出的牛X的框架。 实现了一个虚拟 DOM,用 DOM 的方式将需要的组
- 前言在写程序时,我们会经常碰到程序出现异常,这时候我们就不得不处理这些异常,以保证程序的健壮性。处理异常的版本有以下几种,你通常的做法是哪种
- 翻译自https://pytorch.org/docs/stable/torchvision/models.html主要讲解了torchvi
- 目录机器人api接口调用接口封装机器人实现两个机器人聊天聊天文字转语音总结众所周知,现在网上有很多非常智能bushi(智障)的AI机器人接口
- 你是否苦恼于网上无法下载的“小说在线阅读”内容?或是某些文章的内容让你很有收藏的冲动,却找不到一个下载的链接?是不是有种自己写个程序把全部搞
- 最近在pythonTip做题的时候,遇到了deque模块,以前对其不太了解,现在特此总结一下deque模块是python标准库collect
- Random库主要包含返回随机数的函数,主要用于普通的随机数生成的程序,如果对随机性有特殊要求,比如加密等,可以用函数os.urandom(
- 引言在Babylon.js中媒体设备流使用起来有很多坑,我们将在本文中逐一说明这些坑并提供相应的解决方案。问题1:Property '
- 介绍UliPad是一个国人开发的python轻量级编辑器,导向和灵活的编程器。它如类浏览器,代码自动完成许多功能,如:HTML查看器,目录浏
- 如何导入SQL数据库如何将现成的数据库导入到MySQL中?有两种方式:通过终端命令行语句导入:mysql> source SQL文件的
- 本文为大家分享了购物商城小程序,供大家参考,具体内容如下软件版本:python3.x功能:实现简单购物商城1.允许用户选择购买多
- 1 输出大写字母、小写字母、大小写字母、数字、大小写字母和数字1.1输出小写:找到小写a(97)到z(122)的的ASCII码,然后转义为字
- 一、类型1.变量没有类型,数据有类型例:num = 1 ---->num是没有类型的,1是int类型二、格式化输出2.na