js 数组头部添加_通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events

231ac1d6d8a30a493f9eea53a2af3fcb.png

作者: kaola 程序员成长指北

转发链接:https://mp.weixin.qq.com/s/Y2p7Q0us7HgfJ_RIDH0DNw

前言

为什么写这篇文章?

  • 清楚的记得刚找node工作和面试官聊到了事件循环,然后面试官问事件是如何产生的?什么情况下产生事件。。。
  • Events 在哪些场景应用到了?
  • 之前封装了一个 RxJava 的开源网路请求框架,也是基于发布-订阅模式,语言都是相通的,挺有趣。
  • Events 模块是我公众号 Node.js 进阶路线的一部分

面试会问

说一下 Node.js 哪里应用到了发布/订阅模式

Events 模块在实际项目开发中有使用过吗?具体应用场景是?

Events 监听函数的执行顺序是异步还是同步的?

说几个 Events 模块的常用函数吧?

模拟实现 Node.js 的核心模块 Events

发布/订阅者模式

发布/订阅者模式应该是我在开发过程中遇到的最多的设计模式。发布/订阅者模式,也可以称之为消息机制,定义了一种依赖关系,这种依赖关系可以理解为 1对N (注意:不一定是1对多,有时候也会1对1哦),观察者们同时监听某一个对象相应的状态变换,一旦变化则通知到所有观察者,从而触发观察者相应的事件,该设计模式解决了主体对象与观察者之间功能的 耦合。

生活中的发布/订阅者模式

警察抓小偷

在现实生活中,警察抓小偷是一个典型的观察者模式「这以一个惯犯在街道逛街然后被抓为例子」,这里小偷就是被观察者,各个干警就是观察者,干警时时观察着小偷,当小偷正在偷东西「就给干警发送出一条信号,实际上小偷不可能告诉干警我有偷东西」,干警收到信号,出击抓小偷。这就是一个观察者模式

订阅了某个报社的报纸

生活中就像是去报社订报纸,你喜欢读什么报就去报社去交钱订阅,当发布了新报纸的时候,报社会向所有订阅了报纸的每一个人发送一份,订阅者就可以接收到。

你订阅了我的公众号

我这个微信公号作者是发布者,您这些微信用户是订阅者「我发送一篇文章的时候,关注了【程序员成长指北】的订阅者们都可以收到文章。

实例的代码实现与分析

以大家 订阅公众号为例子,看看 发布/订阅模式如何实现的。(以订阅报纸作为例子的原因,可以增加一个 type参数,用于区分订阅不同类型的公众号,如有的人订阅的是前端公众号,有的人订阅的是 Node.js 公众号,使用此属性来标记。这样和接下来要讲的 EventEmitter 源码更相符,另一个原因是这样你只要打开一个订阅号文章是不是就想到了发布-订阅者模式呢。)

代码如下:

let officeAccounts ={    // 初始化定义一个存储类型对象    subscribes:{        'any':[]    },    // 添加订阅号    subscribe:function(type='any',fn){        if(!this.subscribes[type]){            this.subscribes[type] = [];        }        this.subscribes[type].push(fn);//将订阅方法存在数组中    },    // 退订    unSubscribe:function(type='any',fn){        this.subscribes[type] =         this.subscribes[type].filter((item)=>{            return item!=fn;// 将退订的方法从数组中移除        });    },    // 发布订阅    publish:function(type='any',...args){        this.subscribes[type].forEach(item => {            item(...args);// 根据不同的类型调用相应的方法        });    }}

以上就是一个最简单的观察者模式的实现,可以看到代码非常的简单,核心原理就是将订阅的方法按分类存在一个数组中,当发布时取出执行即可

接下里看小明订阅【程序员成长指北】文章的代码:

let xiaoming = {    readArticle:function (info) {        console.log('小明收到的',info);    }};let xiaogang = {    readArticle:function (info) {        console.log('小刚收到的',info);    }};officeAccounts.subscribe('程序员成长指北',xiaoming.readArticle);officeAccounts.subscribe('程序员成长指北',xiaogang.readArticle);officeAccounts.subscribe('某公众号',xiaoming.readArticle);officeAccounts.unSubscribe('某公众号',xiaoming.readArticle);officeAccounts.publish('程序员成长指北','程序员成长指北的Node文章');officeAccounts.publish('某公众号','某公众号的文章');

运行结果:

小明收到的 程序员成长指北的Node文章小刚收到的 程序员成长指北的Node文章
  • 结论

通过观察现实生活中的三个例子以及代码实例发现发布/订阅模式的确是1对N的关系。当发布者的状态发生改变时,所有订阅者都会得到通知。

57e1562bc10ee837b3af3c45b9879528.png
  • 发布/订阅模式的特点和结构 三要素:
  1. 发布者
  2. 订阅者
  3. 事件(订阅)

发布/订阅者模式的优缺点

  • 优点

主体和观察者之间完全透明,所有的消息传递过程都通过消息调度中心完成,也就是说具体的业务逻辑代码将会是在消息调度中心内,而主体和观察者之间实现了完全的松耦合。对象直接的解耦,异步编程中,可以更松耦合的代码编写。

  • 缺点

程序易读性显著降低;多个发布者和订阅者嵌套在一起的时候,程序难以跟踪,其实还是代码不易读,嘿嘿。

EventEmitter 与 发布/订阅模式的关系

Node.js 中的 EventEmitter 模块就是用了发布/订阅这种设计模式,发布/订阅 模式在主体与观察者之间引入消息调度中心,主体和观察者之间完全透明,所 有的消息传递过程都通过消息调度中心完成,也就是说具体的业务逻辑代码将会是在消息调度中心内完成。

事件的基本组成要素

449ba9357cc53cf09a238b93320d0f65.png

通过Api的对比,来看看Events模块

EventEmitter 定义

Events是 Node.js 中一个使用率很高的模块,其它原生node.js模块都是基于它来完成的,比如流、HTTP等。它的核心思想就是 Events 模块的功能就是一个 事件绑定与触发,所有继承自它的实例都具备事件处理的能力。

EventEs 的一些常用官方API源码与发布/订阅模式对比学习

本模块的官方 Api 讲解不是直接带大家学习文档,而是 通过 对比发布/订阅设计模式自己手写一个版本 Events 的核心代码来学习并记住Api

Events 模块

Events 模块只有一个 EventEmitter 类,首先定义类的基本结构

function EventEmitter() {    //私有属性,保存订阅方法    this._events = {};}//默认设置最大监听数EventEmitter.defaultMaxListeners = 10;module.exports = EventEmitter;

on 方法

on 方法,该方法用于订阅事件(这里 on 和 addListener 说明下),Node.js 源码中这样把它们俩赋值了下,我也不太懂为什么?知道的小伙伴可以告诉我为什么要这样做哦。

EventEmitter.prototype.addListener = function addListener(type, listener) {  return _addListener(this, type, listener, false);};EventEmitter.prototype.on = EventEmitter.prototype.addListener;

接下来是我们对on方法的具体实践:

EventEmitter.prototype.on =    EventEmitter.prototype.addListener = function (type, listener, flag) {        //保证存在实例属性        if (!this._events) this._events = Object.create(null);        if (this._events[type]) {            if (flag) {//从头部插入                this._events[type].unshift(listener);            } else {                this._events[type].push(listener);            }        } else {            this._events[type] = [listener];        }        //绑定事件,触发newListener        if (type !== 'newListener') {            this.emit('newListener', type);        }    };

因为有其它子类需要继承自EventEmitter,因此要判断子类是否存在_event属性,这样做是为了保证人类必须存在此实例属性。而flag标记是一个订阅方法的插入标识,如果为'true'就视为插入在数组的头部。可以看到,这就是观察者模式的订阅方法实现。

emit方法

EventEmitter.prototype.emit = function (type, ...args) {    if (this._events[type]) {        this._events[type].forEach(fn => fn.call(this, ...args));    }};

emit方法就是将订阅方法取出执行,使用call方法来修正this的指向,使其指向子类的实例。

once方法

EventEmitter.prototype.once = function (type, listener) {    let _this = this;    //中间函数,在调用完之后立即删除订阅    function only() {        listener();        _this.removeListener(type, only);    }    //origin保存原回调的引用,用于remove时的判断    only.origin = listener;    this.on(type, only);};

once方法非常有趣,它的功能是将事件订阅“一次”,当这个事件触发过就不会再次触发了。其原理是将订阅的方法再包裹一层函数,在执行后将此函数移除即可。

off方法

EventEmitter.prototype.off =    EventEmitter.prototype.removeListener = function (type, listener) {        if (this._events[type]) {        //过滤掉退订的方法,从数组中移除            this._events[type] =                this._events[type].filter(fn => {                    return fn !== listener && fn.origin !== listener                });        }    };

off方法即为退订,原理同观察者模式一样,将订阅方法从数组中移除即可。

prependListener方法

EventEmitter.prototype.prependListener = function (type, listener) {    this.on(type, listener, true);};

码此方法不必多说了,调用on方法将标记传为true(插入订阅方法在头部)即可。以上,就将EventEmitter类的核心方法实现了。

其它一些不太常用api

  • emitter.listenerCount(eventName)可以获取事件注册的 listener个数
  • emitter.listeners(eventName)可以获取事件注册的 listener数组副本。

Api学习后的小练习

//event.js 文件var events = require('events'); var emitter = new events.EventEmitter(); emitter.on('someEvent', function(arg1, arg2) {     console.log('listener1', arg1, arg2); }); emitter.on('someEvent', function(arg1, arg2) {     console.log('listener2', arg1, arg2); }); emitter.emit('someEvent', 'arg1 参数', 'arg2 参数'); 

执行以上代码,运行的结果如下:

$ node event.jslistener1 arg1 参数 arg2 参数listener2 arg1 参数 arg2 参数

手写代码后的说明

手写Events模块代码的时候注意以下几点:

  • 使用订阅/发布模式
  • 事件的核心组成有哪些
  • 写源码时候考虑一些范围和极限判断

注意:我上面的手写代码并不是性能最好和最完善的,目的只是带大家先弄懂记住它。举个例子:最初的定义EventEmitter类,源码中并不是直接定义 this._events={},请看:

function EventEmitter() {  EventEmitter.init.call(this);}EventEmitter.init = function() {  if (this._events === undefined ||      this._events === Object.getPrototypeOf(this)._events) {    this._events = Object.create(null);    this._eventsCount = 0;  }  this._maxListeners = this._maxListeners || undefined;};

同样是实现一个类,但是源码中更注意性能,我们可能认为简单的一个 this._events={};就可以了,但是通过 jsperf(一个小彩蛋,有需要的搜一下,查看性能工具) 比较两者的性能,源码中高了很多,我就不具体一一讲解了,附上源码地址,有兴趣的可以去学习

lib/events源码地址 https://github.com/nodejs/node/blob/master/lib/events.js

源码篇幅过长,给了地址可以对比继续研究,毕竟是公众号文章,不想被说。但是一些疑问还是要讲的,嘿嘿。

a0c0337d9cb1e79f76c4cc31ee1f94be.png

阅读源码后一些疑问的解释

监听函数的执行顺序是同步 or 异步?

看一段代码:

const EventEmitter = require('events');class MyEmitter extends EventEmitter{};const myEmitter = new MyEmitter();myEmitter.on('event', function() {  console.log('listener1');});myEmitter.on('event', async function() {  console.log('listener2');  setTimeout(() => {    console.log('我是异步中的输出');    resolve(1);  }, 1000);});myEmitter.on('event', function() {  console.log('listener3');});myEmitter.emit('event');console.log('end');

输出结果如下:

// 输出结果listener1listener2listener3end我是异步中的输出

EventEmitter触发事件的时候,各 监听函数的调用是同步的(注意:监听函数的调用是同步的,'end'的输出在最后),但是并不是说监听函数里不能包含异步的代码,代码中listener2那个事件就加了一个异步的函数,它是最后输出的。

事件循环中的事件是什么情况下产生的?什么情况下触发的?

我为什么要把这个单独写成一个小标题来讲,因为发现网上好多文章都是错的,或者不明确,给大家造成了误导。

看这里,某API网站的一段话,具体网站名称在这里就不说了,不想招黑,这段内容没问题,但是对于刚接触事件机制的小伙伴容易混淆

0b7dcaf523b8edd682d03d040ce7b545.png

以 fs.open为例子,看一下到底什么时候产生了事件,什么时候触发,和EventEmitter有什么关系呢?

607705b6e09cad8f0deb7bfaf64a7863.png

流程的一个说明:本图中详细绘制了从 异步调用开始--->异步调用请求封装--->请求对象传入I/O线程池完成I/O操作--->将完成的I/O结果交给I/O观察者--->从I/O观察者中取出回调函数和结果调用执行。

事件产生

关于事件你看图中第三部分,事件循环那里。Node.js 所有的异步 I/O 操作(net.Server, fs.readStream 等)在 完成后都会添加一个事件到事件循环的事件队列中。

事件触发

事件的触发,我们只需要关注图中第三部分,事件循环会在事件队列中取出事件处理。fs.open产生事件的对象都是 events.EventEmitter 的实例,继承了EventEmitter,从事件循环取出事件的时候,触发这个事件和回调函数。

越写越多,越写越想,总是这样,需要控制一下。

33be9915df63015ec38533e19693b529.png

事件类型为error的问题

当我们直接为EventEmitter定义一个error事件,它包含了错误的语义,我们在遇到 异常的时候通常会触发 error 事件。

当 error 被触发时,EventEmitter 规定如果没有响 应的监听器,Node.js 会把它当作异常,退出程序并输出错误信息。

var events = require('events'); var emitter = new events.EventEmitter(); emitter.emit('error'); 

运行时会报错

node.js:201 throw e; // process.nextTick error, or 'error' event on first tick^ Error: Uncaught, unspecified 'error' event. at EventEmitter.emit (events.js:50:15) at Object. (/home/byvoid/error.js:5:9) at Module._compile (module.js:441:26) at Object..js (module.js:459:10) at Module.load (module.js:348:31) at Function._load (module.js:308:12) at Array.0 (module.js:479:10) at EventEmitter._tickCallback (node.js:192:40) 

我们一般要为会触发 error 事件的对象设置监听器,避免遇到错误后整个程序崩溃。

如何修改EventEmitter的最大监听数量?

默认情况下针对单一事件的最大listener数量是10,如果超过10个的话listener还是会执行,只是控制台会有警告信息,告警信息里面已经提示了操作建议,可以通过调用emitter.setMaxListeners()来调整最大listener的限制

(node:9379) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 event listeners added. Use emitter.setMaxListeners() to increase limit

一个打印warn详细内容的小技巧

上面的警告信息的粒度不够,并不能告诉我们是哪里的代码出了问题,可以通过process.on('warning')来获得更具体的信息(emitter、event、eventCount)

process.on('warning', (e) => {  console.log(e);}){ MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 event listeners added. Use emitter.setMaxListeners() to increase limit    at _addListener (events.js:289:19)    at MyEmitter.prependListener (events.js:313:14)    at Object. (/Users/xiji/workspace/learn/event-emitter/b.js:34:11)    at Module._compile (module.js:641:30)    at Object.Module._extensions..js (module.js:652:10)    at Module.load (module.js:560:32)    at tryModuleLoad (module.js:503:12)    at Function.Module._load (module.js:495:3)    at Function.Module.runMain (module.js:682:10)    at startup (bootstrap_node.js:191:16)  name: 'MaxListenersExceededWarning',  emitter:   MyEmitter {     domain: null,     _events: { event: [Array] },     _eventsCount: 1,     _maxListeners: undefined },  type: 'event',  count: 11 }

EventEmitter的应用场景

  • 不能try/catch的错误异常抛出可以使用它
  • 好多常用模块继承自EventEmitter 比如 fs模块 net模块
  • 面试题会考
  • 前端开发中也经常用到发布/订阅模式(思想与Events模块相同)

发布/订阅模式与观察者模式的一点说明

观察者模式与发布-订阅者模式,在平时你可以认为他们是一个东西,但是在某些场合(比如面试)可能需要稍加注意,看一下二者的区别对比

借用网上的一张图

da15bda6d2b9ec329903d2fba81c52c3.png

从图中可以看出,发布-订阅模式中间包含一个Event Channel

  1. 观察者模式 中的观察者和被观察者之间还是存在耦合的,两者必须确切的知道对方的存在才能进行消息的传递。
  2. 发布-订阅模式 中的发布者和订阅者不需要知道对方的存在,他们通过消息代理来进行通信,解耦更加彻底。

推荐JavaScript经典实例学习资料文章

《「前端篇」不再为正则烦恼》

《「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能》

《深入细品浏览器原理「流程图」》

《JavaScript 已进入第三个时代,未来将何去何从?》

《前端上传前预览文件 image、text、json、video、audio「实践」》

《深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系》

《推荐13个有用的JavaScript数组技巧「值得收藏」》

《前端必备基础知识:window.location 详解》

《不要再依赖CommonJS了》

《犀牛书作者:最该忘记的JavaScript特性》

《36个工作中常用的JavaScript函数片段「值得收藏」》

《Node + H5 实现大文件分片上传、断点续传》

《一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」》

《【实践总结】关于小程序挣脱枷锁实现批量上传》

《手把手教你前端的各种文件上传攻略和大文件断点续传》

《字节跳动面试官:请你实现一个大文件上传和断点续传》

《谈谈前端关于文件上传下载那些事【实践】》

《手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件》

《最全的 JavaScript 模块化方案和工具》

《「前端进阶」JS中的内存管理》

《JavaScript正则深入以及10个非常有意思的正则实战》

《前端面试者经常忽视的一道JavaScript 面试题》

《一行JS代码实现一个简单的模板字符串替换「实践」》

《JS代码是如何被压缩的「前端高级进阶」》

《前端开发规范:命名规范、html规范、css规范、js规范》

《【规范篇】前端团队代码规范最佳实践》

《100个原生JavaScript代码片段知识点详细汇总【实践】》

《关于前端174道 JavaScript知识点汇总(一)》

《关于前端174道 JavaScript知识点汇总(二)》

《关于前端174道 JavaScript知识点汇总(三)》

《几个非常有意思的javascript知识点总结【实践】》

《都2020年了,你还不会JavaScript 装饰器?》

《JavaScript实现图片合成下载》

《70个JavaScript知识点详细总结(上)【实践】》

《70个JavaScript知识点详细总结(下)【实践】》

《开源了一个 JavaScript 版敏感词过滤库》

《送你 43 道 JavaScript 面试题》

《3个很棒的小众JavaScript库,你值得拥有》

《手把手教你深入巩固JavaScript知识体系【思维导图】》

《推荐7个很棒的JavaScript产品步骤引导库》

《Echa哥教你彻底弄懂 JavaScript 执行机制》

《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》

《深入解析高频项目中运用到的知识点汇总【JS篇】》

《JavaScript 工具函数大全【新】》

《从JavaScript中看设计模式(总结)》

《身份证号码的正则表达式及验证详解(JavaScript,Regex)》

《浏览器中实现JavaScript计时器的4种创新方式》

《Three.js 动效方案》

《手把手教你常用的59个JS类方法》

《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》

《深入浅出讲解 js 深拷贝 vs 浅拷贝》

《手把手教你JS开发H5游戏【消灭星星】》

《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》

《手把手教你全方位解读JS中this真正含义【实践】》

《书到用时方恨少,一大波JS开发工具函数来了》

《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》

《手把手教你JS 异步编程六种方案【实践】》

《让你减少加班的15条高效JS技巧知识点汇总【实践】》

《手把手教你JS开发H5游戏【黄金矿工】》

《手把手教你JS实现监控浏览器上下左右滚动》

《JS 经典实例知识点整理汇总【实践】》

《2.6万字JS干货分享,带你领略前端魅力【基础篇】》

《2.6万字JS干货分享,带你领略前端魅力【实践篇】》

《简单几步让你的 JS 写得更漂亮》

《恭喜你获得治疗JS this的详细药方》

《谈谈前端关于文件上传下载那些事【实践】》

《面试中教你绕过关于 JavaScript 作用域的 5 个坑》

《Jquery插件(常用的插件库)》

《【JS】如何防止重复发送ajax请求》

《JavaScript+Canvas实现自定义画板》

《Continuation 在 JS 中的应用「前端篇」》

作者: kaola 程序员成长指北

转发链接:https://mp.weixin.qq.com/s/Y2p7Q0us7HgfJ_RIDH0DNw

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值