写点什么

深入浅出 Node.js(四):Node.js 的事件机制

  • 2012-01-31
  • 本文字数:4375 字

    阅读完需:约 14 分钟

专栏的第四篇文章《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 摆脱不掉其他语言同步执行的影响,导致 JavaScript 在后端编程与前端编程之间有着十分显著的差别,在编程模型上无法形成统一。在前端编程中,事件的应用十分广泛,DOM 上的各种事件。在 Ajax 大规模应用之后,异步请求更得到广泛的认同,而 Ajax 亦是基于事件机制的。在 Rhino 中,文件读取等操作,均是同步操作进行的。在这类单线程的编程模型下,如果采用同步机制,无法与 PHP 之类的服务端脚本语言的成熟度媲美,性能也没有值得可圈可点的部分。直到 Ryan Dahl 在 2009 年推出 Node.js 后,后端 JavaScript 才走出其迷局。Node.js 的推出,我觉得该变了两个状况:

  1. 统一了前后端 JavaScript 的编程模型。
  2. 利用事件机制充分利用用异步 IO 突破单线程编程模型的性能瓶颈,使得 JavaScript 在后端达到实用价值。

有了第二次浏览器大战中的佼佼者 V8 的适时助力,使得 Node.js 在短短的两年内达到可观的运行效率,并迅速被大家接受。这一点从 Node.js 项目在 Github 上的流行度和 NPM 上的库的数量可见一斑。

至于 Node.js 为何会选择 Evented I/O for V8 JavaScript 的结构和形式来实现,可以参见一下 2011 年初对作者 Ryan Dahl 的一次采访: http://bostinno.com/2011/01/31/node-js-interview-4-questions-with-creator-ryan-dahl/

事件机制的实现

Node.js 中大部分的模块,都继承自 Event 模块( http://nodejs.org/docs/latest/api/events.html )。Event 模块(events.EventEmitter)是一个简单的事件监听器模式的实现。具有 addListener/on,once,removeListener,removeAllListeners,emit 等基本的事件监听模式的方法实现。它与前端 DOM 树上的事件并不相同,因为它不存在冒泡,逐层捕获等属于 DOM 的事件行为,也没有 preventDefault()、stopPropagation()、 stopImmediatePropagation() 等处理事件传递的方法。

从另一个角度来看,事件侦听器模式也是一种事件钩子(hook)的机制,利用事件钩子导出内部数据或状态给外部调用者。Node.js 中的很多对象,大多具有黑盒的特点,功能点较少,如果不通过事件钩子的形式,对象运行期间的中间值或内部状态,是我们无法获取到的。这种通过事件钩子的方式,可以使编程者不用关注组件是如何启动和执行的,只需关注在需要的事件点上即可。

复制代码
var options = {
host: 'www.google.com',
port: 80,
path: '/upload',
method: 'POST'
};
var req = http.request(options, function (res) {
console.log('STATUS: ' + res.statusCode);
console.log('HEADERS: ' + JSON.stringify(res.headers));
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log('BODY: ' + chunk);
});
});
req.on('error', function (e) {
console.log('problem with request: ' + e.message);
});
// write data to request body
req.write('data\n');
req.write('data\n');
req.end();

在这段 HTTP request 的代码中,程序员只需要将视线放在 error,data 这些业务事件点即可,至于内部的流程如何,无需过于关注。

值得一提的是如果对一个事件添加了超过 10 个侦听器,将会得到一条警告,这一处设计与 Node.js 自身单线程运行有关,设计者认为侦听器太多,可能导致内存泄漏,所以存在这样一个警告。调用:

复制代码
emitter.setMaxListeners(0);

可以将这个限制去掉。

其次,为了提升 Node.js 的程序的健壮性,EventEmitter 对象对 error 事件进行了特殊对待。如果运行期间的错误触发了 error 事件。EventEmitter 会检查是否有对 error 事件添加过侦听器,如果添加了,这个错误将会交由该侦听器处理,否则,这个错误将会作为异常抛出。如果外部没有捕获这个异常,将会引起线程的退出。

事件机制的进阶应用

继承 event.EventEmitter

实现一个继承了 EventEmitter 类是十分简单的,以下是 Node.js 中流对象继承 EventEmitter 的例子:

复制代码
function Stream() {
events.EventEmitter.call(this);
}
util.inherits(Stream, events.EventEmitter);

Node.js 在工具模块中封装了继承的方法,所以此处可以很便利地调用。程序员可以通过这样的方式轻松继承 EventEmitter 对象,利用事件机制,可以帮助你解决一些问题。

多事件之间协作

在略微大一点的应用中,数据与 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 服务器。

为解决这类问题,我曾写作一个模块(EventProxy, https://github.com/JacksonTian/eventproxy )来实现多事件协作,以下为上面代码的改进版:

复制代码
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( https://github.com/JeffreyZhao/jscex )。Jscex 通过运行时编译的思路(需要时也可在运行前编译),将同步思维的代码转换为最终异步的代码来执行,可以在编写代码的时候通过同步思维来写,可以享受到同步思维的便利写作,异步执行的高效性能。如果通过 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

此节感谢 Jscex 作者 @老赵( http://blog.zhaojie.me/ )的指正和帮助。

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

所谓雪崩问题,是在缓存失效的情景下,大并发高访问量同时涌入数据库中查询,数据库无法同时承受如此大的查询请求,进而往前影响到网站整体响应缓慢。那么在 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) 移除掉警告,或者设更大的警告阀值。

参考:

关于作者

田永强,新浪微博 @朴灵,前端工程师,曾就职于 SAP,现就职于淘宝,花名朴灵,致力于 NodeJS 和 Mobile Web App 方面的研发工作。双修前后端 JavaScript,寄望将 NodeJS 引荐给更多的工程师。兴趣:读万卷书,行万里路。个人 Github 地址: http://github.com/JacksonTian


感谢赵劼对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2012-01-31 00:0042729

评论

发布
暂无评论
发现更多内容

Vuex与前端表格施展“组合拳”,实现大屏展示应用的交互增强

葡萄城技术团队

Vue 前端 表格 vuex

网易伏羲实验室入选信通院首批大模型优秀应用案例

网易伏羲

人工智能 网易伏羲

武汉java培训技术学习对学历的要求

小谷哥

兆骑科创创业赛事活动服务平台,投融资服务对接,政策申报

兆骑科创凤阁

缓存穿透、缓存击穿、缓存雪崩及解决方案

Steven

OceanBase 4.0发布:首次实现单机分布式一体化架构、全面上云

OceanBase 数据库

QCN9074 WiFi 6E Card OpenWRT, IPQ6010,802.11ax,wallys,QCN9074 802.11ax 4x4 MU-MIMO 6GHz wifi6E

wallys-wifi6

IPQ6010 QCN9074

毕业设计

Asha

解码微盟集团2022中报:SaaS收入5.81亿逆势增长,高质量增长路径更清晰

ToB行业头条

建木持续集成平台v2.5.3发布

Jianmu

DevOps 持续集成 CI/CD 持续交付 gitops

RocketMQ面试33连问,答完面试官主动要给我提薪资待遇...

Java编程日记

Java 编程 程序员 面试 架构师

提升LED显示屏散热效果的7种方式

Dylan

LED显示屏 led显示屏厂家

前端培训班排名口碑怎么样

小谷哥

华为被迫开源!从认知到落地SpringBoot企业级实战手册(完整版)

Java编程日记

Java 编程 程序员 面试 架构师

元宇宙里的下个社交时代

智捷云

元宇宙

大数据编程培训课程怎么选择

小谷哥

哪家web前端培训班比较好

小谷哥

兆骑科创创业大赛,线上直播路演,高层次人才引进服务平台

兆骑科创凤阁

刘伟光:超大型金融机构国产数据库全面迁移成功实践

OceanBase 数据库

科普达人丨一图看懂阿里云ECS

阿里云弹性计算

云计算 IaaS 弹性计算 ECS

【计算讲谈社】第九讲|“碳中和”时代下:计算的机会在哪里?

大咖说

计算 碳中和

开源一夏 | Node.js实战之Node多进程与JXcore 打包深入运用

恒山其若陋兮

开源 8月月更

元老级的存储类型:块存储,性能很强!

wljslmz

8月月更 块存储

深圳web前端培训费用多少?

小谷哥

VPN是什么?VPN与堡垒机有啥区别?

源字节1号

软件开发 小程序开发

学习总结-网关 架构演进

C++后台开发

网络编程 API 网关 C/C++后台开发 C/C++开发

墙裂安利!用腾讯云AI语音合成打造自己的第一本有声书

牵着蜗牛去散步

腾讯云 腾讯 语音合成 技术实践 有声读物

2022 全球 AI 模型周报

Zilliz

深度学习 计算机视觉 Transformer 多模态

22年Java面试真题整理,一共343道,每一题都很经典,堪称秋招必备

Java编程日记

Java 编程 程序员 面试 架构师

RabbitMQ面试29连问,看完还过不了面试,我给你一Jio

小柴说Java

Java 编程 程序员 面试 架构师

蓝桥杯历届试题 蚂蚁感冒(模拟版+非模拟版)

Five

算法 算法竞赛 8月月更

深入浅出Node.js(四):Node.js的事件机制_JavaScript_田永强_InfoQ精选文章