eventemitter学习

一、什么是EventEmitter?

EventEmitter(事件派发器)是一个对事件进行监听的对象,简单来说就是为事件写回调函数,当触发一个事件执行后,会执行为该事件绑定的回调函数。

JavaScript 事件最核心的包括事件监听 (addListener)、事件触发 (emit)、事件删除 (removeListener),理解如下:

考虑一个DOM事件:

javascript 复制代码
// Typescript
const button = document.querySelector('button');
button.addEventListener("click", (event) => {
    // do something with the event
})

我们向按钮单击事件添加了一个listener (监听器),并且已经订阅了一个正在被发出的事件,当事件发生时会触发回调。每次单击该按钮时,都会发出该事件,而该事件会触发回调。

当处理现有代码库时,或许需要触发自定义事件。不像单击按钮这样的特定DOM事件,而是假设想基于其他触发器发出一个事件,并得到一个事件响应。我们需要一个自定义事件派发器来实现这一点。

事件派发器是一种模式,它监听一个已命名的事件,触发回调,然后发出该事件并附带一个值。有时这被称为"发布/订阅"模型或监听器。它们指的是同一件事。

发布 --- 订阅模式

发布 --- 订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状 态发生改变时,所有依赖于它的对象都将得到通知。

在 JavaScript开发中,我们一般用事件模型来替代传统的发布 --- 订阅模式。

实现的关键要素

  • 发布者有一个订阅者缓存队列

  • 发布者有增加和删除订阅者的方法

  • 发布者状态改变,需要notify方法通知队列中的所有订阅者

  • js中采用事件回调的方式来更新订阅者,因此订阅者不再需要update方法

下面来模拟下EventEmitter的初步实现

kotlin 复制代码
class EventEmitter {
    constructor() {
        this._events = {};//用对象的方式来缓存订阅者队列(事件名称:回调)
    }

    on(eventName, listener) {
        if(typeof listener !== 'function') { return; }
        
        if(!this._events) {//如果只被继承了prototype,需要在继承的对象上添加_events属性
            this._events = Object.create(null);
        }

        if(!this._events[eventName]) {//事件队列不存在
            this._events[eventName] = [];
        }

        this._events[eventName].push(listener);//添加观察者
    }

    addListener(eventName, listener) {
        this.on(eventName, listener);
    }

    removeListener(eventName, listener) {
        if(!this._events[eventName]) { return; }

        this._events[eventName] = this._events[eventName].forEach(item => {
            return item !== listener;
        });
    }

    emmit(eventName, ...args) {//状态改变
        if(!this._events[eventName]) { return; }

        this._events[eventName].forEach(callback => {//通知所有的订阅者,发起回调
            callback.apply(this, args);
        });
    }
}
复制代码

EventEmitter中的once方法可以做到绑定的事件只调用一次,之后不会再被调用,他的实现方式实在怎么样的?正常情况应该是在回调函数被调用一次之后移除这个回调。可以考虑在回调函数上加上once属性,在发起回调的时候判断once是否为真,来确定是否移除这个回调。这样可以达到目的,但是在发起回调时,需要每一次都判断,给通知方法增加了额外的负担,来考虑一个更聪明的实现方式。

wrap函数

javascript 复制代码
once(eventName, listener) {
    function wrap(args) {
        listener.apply(this, args);
        this.removeListener(eventName, wrap);
    }

    wrap.cb = listener;//将回调存储起来用于删除时对比

    this.on(eventName, wrap);
}
复制代码

将回调函数包裹起来,在包裹函数内部移除原回调函数,然后将wrap函数添加进观察者队列。同时要将原回调函数存进wrap中,用在在移除原回调时判断。

修改移除观察者方法

kotlin 复制代码
removeListener(eventName, listener) {
    if(!this._events[eventName]) { return; }

    this._events[eventName] = this._events[eventName].forEach(item => {
        return item !== listener && item.cb !== listener;
    });
}
复制代码

newListener事件

比较有趣的是EventEmitter 同时提供了newListener事件,每次添加观察者(即使是第二次添加newListener)时都会触发这个事件,在on方法中需要添加如下代码:

kotlin 复制代码
this.emmit('newListener', eventName, listener);//触发newListener事件回调
复制代码

defaultMaxListeners

这个静态属性限制了一种事件可以添加的最大回调数量,同时还有配套的setMaxListeners和getMaxListeners方法来设置和获取每个事件可以添加的最大回调数量

javascript 复制代码
setMaxListeners(n) {
    this.maxListeners = n;
}

getMaxListeners() {
    return this.maxListeners ? this.maxListeners : EventEmitter.defaultMaxListeners;
}
复制代码

on方法添加判断;

kotlin 复制代码
if(this._events[eventName].length > this.getMaxListeners()){
    console.warn('超过最大数量,请修改maxListeners')
}
复制代码

二、相关知识

Symbol属性


Symbol是ES6中的添加了一种原始数据类型symbol(已有的原始数据类型:String, Number, boolean, null, undefined, 对象),由于每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就会保证不会出现同名的属性。

这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或者覆盖。

用法举例:

ini 复制代码
let mySymbol = Symbol();
 
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
 
// 第二种写法
let a = {
  [mySymbol]: 'Hello!'
};
 
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
 
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

Object.getOwnPropertySymbols


Object.getOwnPropertySymbols()方法返回一个数组,包含给定对象所有自有的Symbol值的属性(包括不可枚举的Symbol值属性)。

语法

javascript 复制代码
Object.getOwnPropertySymbols(obj); 
// 参数 obj:要获取自有Symbol值属性的对象;返回值一个包含给定对象所有自有的Symbol值的属性的数组。

所有的对象在初始化时都不会包含任何的Symbol值属性,除非在对象上显式定义了Symbol值属性,否则该方法会返回一个空数组。

例:获取对象自有的Symbol值属性

ini 复制代码
var a = Symbol('a');
var b = Symbol('b');
var obj = {};
obj[a] = 1;
obj[b] = 2;
Object.getOwnPropertySymbols(obj); // [Symbol(a), Symbol(b)]

var c = Symbol('c');
Object.defineProperty(obj, c, {
    value: 3,
    enumerate: false,
    writable: false,
    configuration: false
});
Object.getOwnPropertySymbols(obj); // [Symbol(a), Symbol(b), Symbol(c)]

三、源码分析

EventEmitter3是一个典型的第三方事件库,能够让我们自定义实现多个函数与组件间的通信。

1、项目主要内容


本项目的结构比较清晰,主要包括的内容是:

  • 表示单个事件侦听器的EE

  • Prototype属性:保存事件与监听器的_events属性

  • 方法的定义

    1. 为给定事件添加侦听器的addListener方法
    2. 按名称清除事件的clearEvent方法
    3. 与Node.js EventEmitter接口兼容的最小的EventEmitter接口
  • EventEmitter.prototype上的方法定义

    1. eventNames方法:该方法返回一个数组,该数组包含发射器emitter已为其注册侦听器的事件
    2. listeners方法:返回为给定事件注册的侦听器
    3. listenerCount:返回侦听给定事件的侦听器数
    4. emit:调用为给定事件注册的每个侦听器
    5. on:为给定事件添加侦听器
    6. once:为给定事件添加一次性侦听器
    7. removeListener:移除给定事件的侦听器
    8. removeAllListeners:删除所有侦听器或指定事件的侦听器
  • 为removeListener和on方法别名

  • prefix和EventEmitter导出

2、内容详解


下面对内容进行具体讲解

kotlin 复制代码
/**
 * Representation of a single event listener.
 *
 * @param {Function} fn The listener function.
 * @param {*} context The context to invoke the listener with.
 * @param {Boolean} [once=false] Specify if the listener is a one-time listener.
 * @constructor
 * @private
 */
function EE(fn, context, once) {
  this.fn = fn;
  this.context = context;
  this.once = once || false;
}

EE:

  • 这个EventEmitter类保存了监听器方法、上下文和该监听器是否为一次性监听器的once标志(默认不是一次性监听器)
php 复制代码
/**
 * Add a listener for a given event.
 *
 * @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
 * @param {(String|Symbol)} event The event name.
 * @param {Function} fn The listener function.
 * @param {*} context The context to invoke the listener with.
 * @param {Boolean} once Specify if the listener is a one-time listener.
 * @returns {EventEmitter}
 * @private
 */
function addListener(emitter, event, fn, context, once) {
  if (typeof fn !== 'function') {
    throw new TypeError('The listener must be a function');
  }

  var listener = new EE(fn, context || emitter, once)
    , evt = prefix ? prefix + event : event;

  if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++;
  else if (!emitter._events[evt].fn) emitter._events[evt].push(listener);
  else emitter._events[evt] = [emitter._events[evt], listener];

  return emitter;
}

addListener:

  • 通过调用EE类的new方法生成一个listener实例,判断Object.create()方法是否存在,存在则使用该方法创建属性,否则通过为事件增加前缀避免属性覆盖
  • 判断发射器的_events的evt属性,如果该属性为undefined,直接给evt事件注册listener监听器,并增加事件个数
  • 如果发射器_events的evt属性是一个对象,并且已经存在事件监听器,使用push方法将listener注册成为evt事件的监听器
  • 如果发射器上存在给定的事件,事件存在一个与Listener不相等的监听器对象,将evt的监听器转化为包含这两个监听器的数组
php 复制代码
/**
 * Clear event by name.
 *
 * @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
 * @param {(String|Symbol)} evt The Event name.
 * @private
 */
function clearEvent(emitter, evt) {
  if (--emitter._eventsCount === 0) emitter._events = new Events();
  else delete emitter._events[evt];
}

clearEvent:

  • 当发射器只有一个事件时,将事件数量设置为0,且重新生成一个事件
  • 否则直接从发射器删除该事件
javascript 复制代码
/**
 * Minimal `EventEmitter` interface that is molded against the Node.js
 * `EventEmitter` interface.
 *
 * @constructor
 * @public
 */
function EventEmitter() {
  this._events = new Events();
  this._eventsCount = 0;
}

EventEmitter:

  • 定义与Node.js EventEmitter接口兼容的最小的EventEmitter接口,包含存储事件的内存空间和事件数量
javascript 复制代码
/**
 * Return an array listing the events for which the emitter has registered
 * listeners.
 *
 * @returns {Array}
 * @public
 */
EventEmitter.prototype.eventNames = function eventNames() {
  var names = []
    , events
    , name;

  if (this._eventsCount === 0) return names;

  for (name in (events = this._events)) {
    if (has.call(events, name)) names.push(prefix ? name.slice(1) : name);
  }

  // Object.getOwnPropertySymbols()方法返回一个数组,包含给定对象所有自有的Symbol值的属性(包括不可枚举的Symbol值属性)
  if (Object.getOwnPropertySymbols) {
    return names.concat(Object.getOwnPropertySymbols(events));
  }

  return names;
};

EventEmitter.prototype.eventNames:

  • 事件数量为0时返回空数组
  • 否则返回发射器的_events属性中的事件(去除增加的prefix前缀)和_events属性上的Symbol属性事件
ini 复制代码
/**
 * Return the listeners registered for a given event.
 *
 * @param {(String|Symbol)} event The event name.
 * @returns {Array} The registered listeners.
 * @public
 */
EventEmitter.prototype.listeners = function listeners(event) {
  var evt = prefix ? prefix + event : event
    , handlers = this._events[evt];

  if (!handlers) return [];
  if (handlers.fn) return [handlers.fn];

  for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) {
    ee[i] = handlers[i].fn;
  }

  return ee;
};

EventEmitter.prototype.listeners:

  • 如果发射器上没有事件,返回空数组
  • 如果事件上只注册了一个监听器,直接返回包含该监听器的数组
  • 如果事件上注册了多个监听器,遍历存储所有监听器并返回
kotlin 复制代码
/**
 * Return the number of listeners listening to a given event.
 *
 * @param {(String|Symbol)} event The event name.
 * @returns {Number} The number of listeners.
 * @public
 */
EventEmitter.prototype.listenerCount = function listenerCount(event) {
  var evt = prefix ? prefix + event : event
    , listeners = this._events[evt];

  if (!listeners) return 0;
  if (listeners.fn) return 1;
  return listeners.length;
};

EventEmitter.prototype.listenerCount:

  • 事件没有注册监听器时返回0,只有一个监听器时返回1,否则返回监听器数组的长度
ini 复制代码
/**
 * Calls each of the listeners registered for a given event.
 *
 * @param {(String|Symbol)} event The event name.
 * @returns {Boolean} `true` if the event had listeners, else `false`.
 * @public
 */
EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
  var evt = prefix ? prefix + event : event;

  if (!this._events[evt]) return false;

  var listeners = this._events[evt]
    , len = arguments.length
    , args
    , i;

  if (listeners.fn) {
    if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);

    switch (len) {
      case 1: return listeners.fn.call(listeners.context), true;
      case 2: return listeners.fn.call(listeners.context, a1), true;
      case 3: return listeners.fn.call(listeners.context, a1, a2), true;
      case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true;
      case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
      case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
    }

    for (i = 1, args = new Array(len -1); i < len; i++) {
      args[i - 1] = arguments[i];
    }

    listeners.fn.apply(listeners.context, args);
  } else {
    var length = listeners.length
      , j;

    for (i = 0; i < length; i++) {
      if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true);

      switch (len) {
        case 1: listeners[i].fn.call(listeners[i].context); break;
        case 2: listeners[i].fn.call(listeners[i].context, a1); break;
        case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;
        case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break;
        default:
          if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {
            args[j - 1] = arguments[j];
          }

          listeners[i].fn.apply(listeners[i].context, args);
      }
    }
  }

  return true;
};

EventEmitter.prototype.emit:

  • 当触发给定事件时,只需要调用emit方法。该方法会自动检索事件event中所有的事件监听器,触发所有的事件监听函数,同时移除掉通过once添加的一次性监听器
vbnet 复制代码
/**
 * Add a listener for a given event.
 *
 * @param {(String|Symbol)} event The event name.
 * @param {Function} fn The listener function.
 * @param {*} [context=this] The context to invoke the listener with.
 * @returns {EventEmitter} `this`.
 * @public
 */
EventEmitter.prototype.on = function on(event, fn, context) {
  return addListener(this, event, fn, context, false);
};

EventEmitter.prototype.on:

  • 通过调用addListener方法为指定的事件指定上下文并注册监听器
javascript 复制代码
/**
 * Add a one-time listener for a given event.
 *
 * @param {(String|Symbol)} event The event name.
 * @param {Function} fn The listener function.
 * @param {*} [context=this] The context to invoke the listener with.
 * @returns {EventEmitter} `this`.
 * @public
 */
EventEmitter.prototype.once = function once(event, fn, context) {
  return addListener(this, event, fn, context, true);
};

EventEmitter.prototype.once:

  • 通过调用addListener方法为指定的事件指定上下文并注册监听器,且通过设置once属性值为true将该监听器指定为一次性监听器
kotlin 复制代码
/**
 * Remove the listeners of a given event.
 *
 * @param {(String|Symbol)} event The event name.
 * @param {Function} fn Only remove the listeners that match this function.
 * @param {*} context Only remove the listeners that have this context.
 * @param {Boolean} once Only remove one-time listeners.
 * @returns {EventEmitter} `this`.
 * @public
 */
EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {
  var evt = prefix ? prefix + event : event;

  if (!this._events[evt]) return this;
  if (!fn) {
    clearEvent(this, evt);
    return this;
  }

  var listeners = this._events[evt];

  if (listeners.fn) {
    if (
      listeners.fn === fn &&
      (!once || listeners.once) &&
      (!context || listeners.context === context)
    ) {
      clearEvent(this, evt);
    }
  } else {
    for (var i = 0, events = [], length = listeners.length; i < length; i++) {
      if (
        listeners[i].fn !== fn ||
        (once && !listeners[i].once) ||
        (context && listeners[i].context !== context)
      ) {
        events.push(listeners[i]);
      }
    }

    //
    // Reset the array, or remove it completely if we have no more listeners.
    //
    if (events.length) this._events[evt] = events.length === 1 ? events[0] : events;
    else clearEvent(this, evt);
  }

  return this;
};

EventEmitter.prototype.removeListener:

  • 如果发射器上不存在该事件,直接返回发射器
  • 事件只存在一个监听器时,调用clearEvent方法删除该监听器
  • 否则使用一个event属性来保存不需要被移除的事件监听对象,然后遍历整个事件监听器数组,并且最后将event属性的值赋值给_event属性从而覆盖掉原有的属性来删除指定的监听器
ini 复制代码
/**
 * Remove all listeners, or those of the specified event.
 *
 * @param {(String|Symbol)} [event] The event name.
 * @returns {EventEmitter} `this`.
 * @public
 */
EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {
  var evt;

  if (event) {
    evt = prefix ? prefix + event : event;
    if (this._events[evt]) clearEvent(this, evt);
  } else {
    this._events = new Events();
    this._eventsCount = 0;
  }

  return this;
};

EventEmitter.prototype.removeAllListeners:

  • 如果给定的事件存在,给该事件添加前缀,通过调用clearEvent方法将发射器上增加前缀的方法删除
  • 否则直接给发射器的_events属性指定一个Events实例并设置事件数量为0
javascript 复制代码
//
// Alias methods names because people roll like that.
//
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
EventEmitter.prototype.addListener = EventEmitter.prototype.on;

别名:

  • 照顾用户习惯,方便用户使用,为EventEmitter.prototyp的removeListener和on方法别名
java 复制代码
//
// Expose the prefix.
//
EventEmitter.prefixed = prefix;

//
// Allow `EventEmitter` to be imported as module namespace.
//
EventEmitter.EventEmitter = EventEmitter;

//
// Expose the module.
//
if ('undefined' !== typeof module) {
  module.exports = EventEmitter;
}

参考: segmentfault.com/a/119000003...

juejin.cn/post/684490...

juejin.cn/post/711912...

相关推荐
她似晚风般温柔7894 小时前
Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)
开发语言·javascript·uni-app
Jiaberrr5 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy5 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
Ylucius6 小时前
动态语言? 静态语言? ------区别何在?java,js,c,c++,python分给是静态or动态语言?
java·c语言·javascript·c++·python·学习
200不是二百6 小时前
Vuex详解
前端·javascript·vue.js
LvManBa6 小时前
Vue学习记录之三(ref全家桶)
javascript·vue.js·学习
深情废杨杨6 小时前
前端vue-父传子
前端·javascript·vue.js
司篂篂8 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客8 小时前
pinia在vue3中的使用
前端·javascript·vue.js
Jiaberrr10 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选