写点什么

深入浅出 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:0042006

评论

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

LeetCode题解:61. 旋转链表,闭合为环,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

阿里云飞天论文获国际架构顶会 ATC 2021最佳论文:全球仅三篇

阿里云大数据AI技术

我们向华为公司学什么?

石云升

学习 华为 7月日更

我看 JAVA 之 并发编程【二】java.util.concurrent.locks

awen

Java AQS lock Condition LockSupport

阿里内网疯传的P8“顶级”分布式架构手册,GitHub上线直接霸榜了

java小李

微信业务架构 P8

秀出新天际的SpringBoot笔记,让开发像搭积木一样简单

java小李

Spring Boot java架构

阿里云技术三面Java岗,只需半个月,就可以成功拿25k offer ,轻松任职阿里云

愚者

Java 阿里云

Fil还有希望吗?目前Fil发展如何了?

区块链 IPFS Filecoin fil filecoin生态

香到爆!SpringBoot/SpringCloud全套学习脑图+面试笔记免费分享

java小李

SpringCloud Alibaba

阿里大牛耗时三年整理出来的4588页Java面试诛仙手册,已全面开源

java小李

Linux 面试

对话交互:封闭域任务型与开放域闲聊算法技术

OPPO小布助手

人工智能 深度学习 对话 智能助手 语义理解

模块四作业

燕燕 yen yen

架构实战营

拿来吧你!从阿里P8手里抢来的的JDK源码解析手册,Alibaba真的强

java小李

jdk

阿里巴巴中高级java面试题详解,吃透这20道面试题,offer拿到你手软

愚者

Java 面试

柏益美康系统开发案例详解,柏益美康开发源码

系统开发咨询1357O98O718

短视频营销系统开发内容

阿里这份15w字Java核心面试笔记!GitHub凭借百万下载量位居榜首

java小李

面试 Java核心笔记

大专的我狂刷29天“阿里内部面试笔记”最终直接斩获十七个Offer

java小李

大数据 面试

阿里资深架构师倾情力荐:Java全线成长宝典,P5到P8一应俱全

愚者

Java 面试

独家!精挑细选三个月的臻品Java面试题,无糟粕!高质量

白亦杨

Java 编程 程序员 架构师 计算机

纷多多拼团系统开发案例详解,纷多多拼团现成源码

系统开发咨询1357O98O718

HarmonyOS学习路之开发篇——线程管理

爱吃土豆丝的打工人

多线程 HarmonyOS 线程管理

对于大部分Java开发者说“60W年薪是无法逾越沟壑”你赞同吗?

愚者

Java 后端

阿里内网流传的9w字图解网络(全彩版)GitHub现已下载量过百万

java小李

HTTP

北鲲云超算在生命科学领域的使用场景中有什么作用?

北鲲云

史上最全面Java面试指南手册宝典!轻松拿腾讯等大厂offer

愚者

Java 面试

声网 Agora 音频互动 MoS 分方法:为音频互动体验进行实时打分

声网

算法 网络

泪目!跳槽太不容易,蚂蚁金服三轮面试,四个小时灵魂拷问

java小李

面试 Leader

系统性能优化-数据结构

短视频平台获客软件系统开发

渣本展示Spring Cloud 架构绝活!最后成功入职阿里

java小李

Spring Cloud

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