资讯专栏INFORMATION COLUMN

Node 之 Event 模块

mrli2016 / 1359人阅读

摘要:为什么把叫做集合而不能称为严格意义上的对象,来看这个集合的构造函数可以见得,是与处于同一层级的而非是继承自,所以说由实例出来的对象更加的纯净,并没有诸如等方法,更像是一个集合。

写在前面

事件的编程方式具有轻量级、松耦合、只关注事务点等优势,在浏览器端,有着自己的一套DOM事件机制,其中含包括这诸如事件冒泡,事件捕获等;然而Node的事件机制没有事件冒泡等,其原理就是设计模式中的观察者模式Node很多的模块继承这个事件模块,下面我们就来根据源码来学习下其API,做到知其然更知其所以然。

引入模块
const EventEmitter = require("events");
const EventEmitter = require("events").EventEmitter;

经常会看到这种两种方式来引入我们的events模块,但是在Node的高版本中可以直接使用第一种方式,高版本也支持下面这种方式,下面的这种方式主是在Node0.10.x版本时使用,源码中也是很清楚,之所以这么做就是为了兼容低版本时写下的Node代码:

module.exports = EventEmitter;
EventEmitter.EventEmitter = EventEmitter;

注:之后提到的objEventEmitter的实例对象,也就是obj = new EventEmitter()

基本使用

得到我们的事件构造函数后,我们就可以来实例化一个事件对象:

const EventEmitter = require("events"),
    follow = new EventEmitter();
follow.on("node", question => {
    console.log(`有一个关于node的问题: ${question}`);
});
follow.emit("node", "jade与ejs选择哪个?");

这是一个简单的使用,下面我们就来看看我们所用到的API以及它们的实现:

订阅事件

on(type, listener)来订阅事件,传入的type参数为事件名,listener为待发布函数。同时addListener方法和on方法有着同样的效果,指向的是内存的同一块:

EventEmitter.prototype.on = EventEmitter.prototype.addListener;

在调用on时,假若我们订阅了newListener事件,该事件会先被发布。

那么问题来了?订阅的事件被存储在了哪里呢?

答案就是obj._events,这是一个事件集合,事件名就是该集合的键名,当事件的待补发函数只有一个时,键值为函数;当有多个时,键值为数组。为什么把obj._events叫做集合而不能称为严格意义上的对象,来看这个集合的构造函数:

function EventHandlers () {}
EventHandlers.prototype = Object.create(null);

可以见得,EventHandlers.prototype是与Object.prototype处于同一层级的而非是继承自Object.prototype,所以说由EventHandlers实例出来的对象更加的"纯净",并没有诸如toString等方法,更像是一个集合。

随着给一个事件添加待发布函数,当添加的数量超过10条是,会发现有警告:

(node) warning: possible EventEmitter memory leak detected. 11 git listeners added. Use emitter.setMaxListeners() to increase limit.

产生警告的原因就是事件待发布函数数组的长度超过了默认的最大容量,默认的最大容量是EventEmitter.defaultMaxListeners,而这个属性是一个getter/setter访问器,访问的是变量defaultMaxListeners的值,也就是10。

// 得到最大容量
function $getMaxListeners (that) {
    if (that._maxListeners === undefined) 
        return EventEmitter.defaultMaxListeners;
    return that._maxListeners;
}
// 发出警告代码:
if (!existing.warned) {
    m = $getMaxListeners(target);
    if (m && m > 0 && existing.length > m) {
        existing.warned = true;
      process.emitWarning("Possible EventEmitter memory leak detected. " + `${existing.length} ${type} listeners added. ` + "Use emitter.setMaxListeners() to increase limit");
    }
}

观察获得最大容量函数可以发现,给obj._maxListeners赋值可以提升我们的最大容量(obj._maxListeners初始化时被赋值为undefined),可以利用setMaxListeners(n)方法来进行赋值:

EventEmitter.prototype.setMaxListeners = function (n) {
    if (typeof n !== "number" || n < 0 || isNaN(n))
        throw new TypeError("n must be a position number");
    this._maxListeners = n;
    return this;
};

看源码可以发现,订阅事件其实是用的_addListener函数,其最后一个参数为prepend,代表着是否将待发布函数添加到事件数组的第一个,所以应该还有一个prependListener(type, listener)函数,可以将listener添加到obj.events.type的第一个位置。

once(type, listener),通过这种方式添加的待发布函数,只能被发布一次,发布一次后就会被移除。

// 将listener包裹在一个g函数中,在每次执行时,现将该函数从事件数组中移除
// 真正的待发布函数成为了g函数的属性
function _onceWrap (target, type, listener) {
    var fired = false;
    function g () {
        target.removeListener(type, g);  // 先移除
        if (!fired) {
            fired = true;
            listener.apply(target, arguments);  // 再发布
        }
    }
    g.listener = listener;
    return g;
}

于此对应的还有prependOnceListener方法,下面来看一个例子:

work.once("git", pull);
work.on("git", () => {
    console.log("git status");
});
work.emit("git");
console.log("第二次");
work.emit("git");
// git pull
// git status
// 第二次
// git status
发布事件

emit(type, args)来进行事件的发布,在实现上也很简单就是执行obj.events.type的函数或者遍历obj.events.type数组一次执行函数,需要注意的是error事件的发布,如果没有订阅error事件的话,发布时,就会用到throw new Error()

移除事件

removeListener(type, listener)来移除${type}事件的listener函数

removeAllListeners(type),当传入type时会将type事件全部移除;不传入参数时,会将obj._events重置。

在移除时,假若给obj订阅了removeListener事件的话,那么在每移除一个待发布函数时,会发布一次该事件,在将obj重置时,也会最后将该事件移除。

function pull () {
    console.log("git pull");
};
work.on("removeListener", (type, fn) => {
    console.log(`remove ${type} ${fn.name} event`);
})
work.on("git", pull);
work.on("git", () => {
    console.log("git status");
});

work.removeListener("git", pull);
work.emit("git");
// 依次输出
// remove git pull event
// git status
其余API

eventNames,返回实例对象所有的事件名数组或者一个空数组,源码中利用了ES6的新方法Reflect.ownKeys来获得obj._events对象的自身属性:

EventEmitter.prototype.eventNames = function eventNames() {
  return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : [];
};

listenerCount(type)返回事件的待发布函数的的数量,也就是obj._events.length or 1,这个方法在obj上和EventEmitter都有,本质上都是调用下面这个方法,实现也是很明了:

function listenerCount (type) {
    const events = this._events;
    
    if (events) {
        const evlistener = events[type];

        if (typeof evlistener === "function") {
            return 1;
        } else if (evlistener) {
            return evlistener.length;
        }
    }
}

listeners(type),返回${type}事件的待发布函数数组或者空数组,需要注意是这个数组并不是obj.events.type的引用。

总结

这次阅读Node的源代码发现,Node源码中对于原生的slicesplice并没有使用,而是自己写了一个针对性更加强的arrayClonespliceOne函数,不知这样写的原因是不是要将速度提升,因为看V8源码会发现,slicesplice的实现有一些复杂,都有额外的判断来对参数进行规范化,像Node源码自己写的话,减少了这些无用的判断,从而提升了效率。当然这只是我个人的一些理解,如有错误还请大家指出。

附:解析的events.js

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/87862.html

相关文章

  • 前端小项目在线便利贴

    摘要:实现的效果如下界面可能不是太好看,考虑到容器的高度会被拉长,因此没有用图片做背景。 实现的效果如下: showImg(https://segmentfault.com/img/remote/1460000011155402); 界面可能不是太好看?,考虑到容器的高度会被拉长,因此没有用图片做背景。 预览 便利贴 涉及的知识点 sass(css 预编译器) webpack(自动化构...

    microelec 评论0 收藏0
  • Node.js Event LoopTimers, process.nextTick()

    摘要:前言以异步和事件驱动的特性著称但异步是怎么实现的呢其中核心的一部分就是下文中内容基本来自于文档有不准确地方请指出什么是能让的操作表现得无阻塞尽管是单线程的但通过尽可能的将操作放到操作系统内核由于现在大多数内核都是多线程的它们可以在后台执行多 前言 Node.js以异步I/O和事件驱动的特性著称,但异步I/O是怎么实现的呢?其中核心的一部分就是event loop,下文中内容基本来自于N...

    sarva 评论0 收藏0
  • Swoole 源码分析——Server模块Timer模块与时间轮算法

    摘要:当其就绪时,会调用执行定时函数。进程超时停止进程将要停止时,并不会立刻停止,而是会等待事件循环结束后停止,这时为了防止进程不退出,还设置了的延迟,超过就会停止该进程。当允许空闲时间小于时,统一每隔检测空闲连接。 前言 swoole 的 timer 模块功能有三个:用户定时任务、剔除空闲连接、更新 server 时间。timer 模块的底层有两种,一种是基于 alarm 信号,一种是基于...

    qieangel2013 评论0 收藏0
  • 读Zepto源码Gesture模块

    摘要:模块基于上的事件的封装,利用属性,封装出系列事件。这个判断需要引入设备侦测模块。然后是监测事件,根据这三个事件,可以组合出和事件。其中变量对象和模块中的对象的作用差不多,可以先看看读源码之模块对模块的分析。 Gesture 模块基于 IOS 上的 Gesture 事件的封装,利用 scale 属性,封装出 pinch 系列事件。 读 Zepto 源码系列文章已经放到了github上,欢...

    coolpail 评论0 收藏0
  • 【译】Node.js 前端开发指南

    摘要:定时器在和浏览器中的表现形式是相同的。关于定时器的一个重要的事情是,我们提供的延迟不代表在这个时间之后回调就会被执行。它的真正含义是,一旦主线程完成所有操作包括微任务并且没有其它具有更高优先级的定时器,将在此时间之后执行回调。 众成翻译 原文链接 关于作者 2018年6月21日出版​ 本指南面向了解Javascript但尚未十分熟悉Node.js的前端开发人员。我这里不专注于语言本身...

    CntChen 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<