EventEmitter 源码阅读

Author Avatar
Hongxu 8月 27, 2018

上周看了 EventEmitter 的源码,这篇博客打算做个小结(不是 Node 的源码,只是 github 上搜到的星星最多的项目,还有个项目 EventEmitter3 项目作者目前依然在维护,主打性能,看起来非常不错)

整体结构

;(function(exports) {
    'use strict';

    function EventEmitter() {}

    var proto = EventEmitter.prototype;
    var originalGlobalValue = exports.EventEmitter;

    /**
     * 工具函数
     * indexOfListener
     * alias
     * isValidListener
     */

    /**
     * 方法实现
     */

    EventEmitter.noConflict = function() {
        exports.EventEmitter = originalGlobalValue;
        return EventEmitter;
    }

    if (typeof define === 'function' && define.amd) {
        define(function() {
            return EventEmitter;
        });
    } else if (typeof module === 'object' && module.exports){
        module.exports = EventEmitter;
    } else {
        exports.EventEmitter = EventEmitter;
    }

}(typeof window !== 'undefined' ? window : this || {}));

最外层的自执行

;(function(exports){
  // some code  
}(typeof window !== 'undefined' ? window : this || {}));

0. 闭包

避免全区变量污染,私有作用域… 就是闭包的常规用途

1. 开头的分号

为了防止打包或则其他工具合并 js 代码并压缩的时候由于前面没有以分号结尾而出错,比如这种情况 var a = someFunction

如果不加分号则会变成 var a = someFunction( function(){}() ),这可能导致意外。

2. context 以 export 为变量名传入函数

可以在代码压缩时减小 size,将全局的 window 传入可以避免内部使用 window 进行查询时由作用域链向上多找一级。

3. 类的实现

利用函数的原型链模拟,先创建一个空函数 function EventEmitter() {},然后将该类的方法挂载到 EventEmitter.prototype 上面。

4. 对于冲突的处理

    var originalGlobalValue = exports.EventEmitter;

    EventEmitter.noConflict = function() {
        exports.EventEmitter = originalGlobalValue;
        return EventEmitter;
    }

先将全局的 EventEmitter 保存到一个变量 originalGlobalValue 中。执行 noConflict 后再将 originalGlobalValue 还给全局的 EventEmitter,并返回这个库所实现的 EventEmitter

例如:

var EventEmitter = function a() {}

// load myEventEmitter.js
// 此时 EventEmitter = myEventEmitter;
var b = EventEmitter.noConflict();

// 此时 EventEmitter = function a() {}
// b = myEventEmitter

5. 对于模块化的处理

// 兼容 AMD
if (typeof define === 'function' && define.amd) {
        define(function() {
            return EventEmitter;
        });
    }
    // 兼容 CommonJS
    else if (typeof module === 'object' && module.exports){
        module.exports = EventEmitter;
    } else {
        exports.EventEmitter = EventEmitter;
    }

这个库的整体结构并不复杂,基本上看过 jQuery 源码或则对一些常用的模块化方法有基本了解都能理解

工具函数的实现

alias

/**
 * Alias a method while keeping the context correct, to allow for overwriting of target method.
 *
 * @param {String} name The name of the target method.
 * @return {Function} The aliased method
 * @api private
 */
function alias(name) {
    return function aliasClosure() {
        return this[name].apply(this, arguments);
    };
}

这么调用 EventEmitter.prototype.on = alise('addListener')
可是为什么要多此一举? 而不直接用 EventEmitter.prototype.on = EventEmitter.prototype.addListener

其他

isValidListener(listener) 就是判断 listener 是不是合法

indexOfListener(listeners: Function[], listener: Function) 就是在数组中找到对应项的 listener 属性和第二个参数 Function 全等的项并返回索引值

核心方法的实现

_getEvents

/**
 * Fetches the events object and creates one if required.
 *
 * @return {Object} The events storage object.
 * @api private
 */
proto._getEvents = function _getEvents() {
    return this._events || (this._events = {});
};

_event 属性如果不存在就创建一个空对象赋值给 _event 并返回。

getListeners

/**
 * Returns the listener array for the specified event.
 * Will initialise the event object and listener arrays if required.
 * Will return an object if you use a regex search. The object contains keys for each matched event. So /ba[rz]/ might return an object containing bar and baz. But only if you have either defined them with defineEvent or added some listeners to them.
 * Each property in the object response is an array of listener functions.
 *
 * @param {String|RegExp} evt Name of the event to return the listeners from.
 * @return {Function[]|Object} All listener functions for the event.
 */
proto.getListeners = function getListeners(evt) {
    var events = this._getEvents();
    var response;
    var key;

    // Return a concatenated array of all matching events if
    // the selector is a regular expression.
    // 支持正则查询, 如果没查询到返回空对象 {}
    if (evt instanceof RegExp) {
        response = {};
        for (key in events) {
            if (events.hasOwnProperty(key) && evt.test(key)) {
                response[key] = events[key];
            }
        }
    }
    else {
        response = events[evt] || (events[evt] = []); // 如果不存在就初始化 _this.event[evt] 为 []
    }

    return response;
};

addListener

/**
 * Adds a listener function to the specified event.
 * The listener will not be added if it is a duplicate.
 * If the listener returns true then it will be removed after it is called.
 * If you pass a regular expression as the event name then the listener will be added to all events that match it.
 *
 * @param {String|RegExp} evt Name of the event to attach the listener to.
 * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling.
 * @return {Object} Current instance of EventEmitter for chaining.
 *
 * 1. 先判断要添加的 listener 是否合法
 * 2. 获取当前符合条件的 listener
 * 3. 判断要添加的 listener 是否已经存在
 * 4. 如果不存在 就将 listener 包裹成固定的格式起来放入 listeners 中
 */
proto.addListener = function addListener(evt, listener) {
    if (!isValidListener(listener)) {
        throw new TypeError('listener must be a function');
    }

    var listeners = this.getListenersAsObject(evt);
    var listenerIsWrapped = typeof listener === 'object';
    var key;

    for (key in listeners) {
        if (listeners.hasOwnProperty(key) && indexOfListener(listeners[key], listener) === -1) {
            listeners[key].push(listenerIsWrapped ? listener : {
                listener: listener,
                once: false
            });
        }
    }

    return this;
};

removeListener

/**
 * Removes a listener function from the specified event.
 * When passed a regular expression as the event name, it will remove the listener from all events that match it.
 *
 * @param {String|RegExp} evt Name of the event to remove the listener from.
 * @param {Function} listener Method to remove from the event.
 * @return {Object} Current instance of EventEmitter for chaining.
 */
// 删除事件订阅
// 1. 找到所有的符合 evt 规则的事件
// 2. 循环找出事件在 events 中的索引并 splice 删除
proto.removeListener = function removeListener(evt, listener) {
    var listeners = this.getListenersAsObject(evt);
    var index;
    var key;

    for (key in listeners) {
        if (listeners.hasOwnProperty(key)) {
            index = indexOfListener(listeners[key], listener);

            if (index !== -1) {
                listeners[key].splice(index, 1);
            }
        }
    }

    return this;
};

removeEvent

/**
 * Removes all listeners from a specified event.
 * If you do not specify an event then all listeners will be removed.
 * That means every event will be emptied.
 * You can also pass a regex to remove all events that match it.
 *
 * @param {String|RegExp} [evt] Optional name of the event to remove all listeners for. Will remove from every event if not passed.
 * @return {Object} Current instance of EventEmitter for chaining.
 */
/**
 * 删除事件
 * 1. 判断 evt 类型
 * 如果是 字符串 就直接删除 _events 中对应的属性
 * 如果是 正则表达式 就删除符合正则的 _events 中对应的属性
 * 如果都不是 就删除全部事件
 */
proto.removeEvent = function removeEvent(evt) {
    var type = typeof evt;
    var events = this._getEvents();
    var key;

    // Remove different things depending on the state of evt
    if (type === 'string') {
        // Remove all listeners for the specified event
        delete events[evt];
    }
    else if (evt instanceof RegExp) {
        // Remove all events matching the regex.
        for (key in events) {
            if (events.hasOwnProperty(key) && evt.test(key)) {
                delete events[key];
            }
        }
    }
    else {
        // Remove all listeners in all events
        delete this._events;
    }

    return this;
};

emitEvent

/**
 * Emits an event of your choice.
 * When emitted, every listener attached to that event will be executed.
 * If you pass the optional argument array then those arguments will be passed to every listener upon execution.
 * Because it uses `apply`, your array of arguments will be passed as if you wrote them out separately.
 * So they will not arrive within the array on the other side, they will be separate.
 * You can also pass a regular expression to emit to all events that match it.
 *
 * @param {String|RegExp} evt Name of the event to emit and execute listeners for.
 * @param {Array} [args] Optional array of arguments to be passed to each listener.
 * @return {Object} Current instance of EventEmitter for chaining.
 */
/**
 * 触发事件
 * 1. 找到 符合条件的事件
 * 2. 依次调用
 * 3. 判断是否是一次调用,如果是就删除事件
 */
proto.emitEvent = function emitEvent(evt, args) {
    var listenersMap = this.getListenersAsObject(evt);
    var listeners;
    var listener;
    var i;
    var key;
    var response;

    for (key in listenersMap) {
        if (listenersMap.hasOwnProperty(key)) {
            listeners = listenersMap[key].slice(0);

            for (i = 0; i < listeners.length; i++) {
                // If the listener returns true then it shall be removed from the event
                // The function is executed either with a basic call or an apply if there is an args array
                listener = listeners[i];

                if (listener.once === true) {
                    this.removeListener(evt, listener.listener);
                }

                response = listener.listener.apply(this, args || []);

                if (response === this._getOnceReturnValue()) {
                    this.removeListener(evt, listener.listener);
                }
            }
        }
    }

    return this;
};

全部方法

  • _getEvents
  • _getOnceReturnValue
  • getListeners
  • flattenListeners
  • getListenersAsObject
  • addListener || on
  • addOnceListener || once
  • defineEvent
  • defineEvents
  • removeListener || off
  • addListeners
  • removeListeners
  • manipulateListeners
  • removeEvent || removeAllListeners
  • emitEvent || trigger
  • emit
  • setOnceReturnValue

之后可以自造一个实现该功能的轮子