【锁定直播】字节、华为云、阿里云等技术专家讨论如何将大模型接入 AIOps 解决实际问题,戳>>> 了解详情
写点什么

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

评论

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

使用大型语言模型进行自主视觉信息搜索

3D建模设计

LLM 大语言模型

领域驱动设计(DDD): 三层架构到DDD架构演化

付威

领域驱动设计

CloudEon入选开源中国最有价值开源项目(GVP)

CloudEon开源

C++函数重载

攻城狮Wayne

精进语言模型:探索LLM Training微调与奖励模型技术的新途径

汀丶人工智能

自然语言处理 强化学习 深度强化学习 LLM模型

服务质量不能掉链子,中小企业采购云服务器为何更要选择大厂?

YG科技

ARTS 打卡第 13 天

自由

ARTS 打卡计划

利用 XGBoost 进行时间序列预测

3D建模设计

机器学习

低代码开发平台能开发什么类型的系统和软件?

优秀

低代码开发平台

当高并发来袭:StarRocks Query Cache 一招搞定!

StarRocks

数据库 后端 高并发 StarRocks 湖仓一体

金融场景的“四边形战士”,容联云赤兔大模型向行业飞驰

脑极体

金融 AI大模型

YashanDB获强制性国家标准GB 18030-2022最高级别认证

YashanDB

第二期YCA认证培训圆满结束!

YashanDB

如何使用图形数据库构建实时推荐引擎

3D建模设计

数据分析 数据工程

制造业为什么要数字化?

优秀

制造业数字化

AIGC如何借AI Agent落地?TARS-RPA-Agent破解RPA与LLM融合难题

王吉伟频道

RPA AIGC autogpt AI大语言模型 AI Agent

中小企业建站,采购云服务器上需注意什么?

YG科技

使用 OpenAI GPT 模型的最佳实践

3D建模设计

人工智能 openai GPT

对 JDK8 新出的Optional类的探索与思考

emanjusaka

Java jdk 后端

【高危】致远A8前台上传解压漏洞 (MPS-6tdh-8qpu)

墨菲安全

网络安全 漏洞 致远

对话无服务器专家 Luca Mezzalira:你真的为 Serverless × AI 做好准备了吗?

亚马逊云科技 (Amazon Web Services)

Java 人工智能

采购云服务器安全与成本不可兼得?云耀云服务器L或许可以了解一下

轶天下事

机器学习会取代数据科学吗?

3D建模设计

机器学习

连接未来 驱动创新|腾讯云 CODING DevOps 主题沙龙诚邀您的参与

CODING DevOps

企业新业务拓展,云耀云服务器L实例能否助其进行快速开发

YG科技

初创公司预算有限,在云服务器选择上应该如何选择?

YG科技

Presto 设计与实现(七):Event

冰心的小屋

数据湖 Event presto presto 设计与实现

谁能代表中国软件的未来

B Impact

【严重】Smartbi windowUnloading 限制绕过导致远程代码执行 (MPS-e2z8-wdi6)

墨菲安全

网络安全 漏洞

【高危】WPS Office 远程代码执行漏洞(WPSSRC-2023-0701绕过) (MPS-qjky-hw9x)

墨菲安全

网络安全 漏洞 WPS Office

火山引擎云调度GTM“同城容灾”与“异地多活”实践

火山引擎边缘云

容灾 容灾备份 容灾多活

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