JavaScript EventEmitter

GitHub地址:JavaScript EventEmitterjavascript

博客地址:JavaScript EventEmitterhtml

水平有限,欢迎批评指正java

2个多月前把 Github 上的 eventemitter3Node.js 下的事件模块 events 的源码抄了一遍,才终于对 JavaScript 事件有所了解。node

上个周末花点时间根据以前看源码的理解本身用 ES6 实现了一个 eventemitter8,而后也发布到 npm 上了,让我比较意外的是才发布两天在没有 readme 介绍,没有任何宣传的状况下竟然有45个下载,我很好奇都是谁下载的,会不会用。我花了很多时间半抄半原创的一个 JavaScript 时间处理库 now.js (npm 传送门:now.js) ,在我大力宣传的状况下,4个月的下载量才177。真是有心栽花花不开,无意插柳柳成荫git

eventemitter8 大部分是我根据看源码理解后写出来的,有一些方法如listenerslistenerCounteventNames 一会儿想不起来到底作什么,回头重查。测试用例很多是参考了 eventemitter3,在此对 eventemitter3 的开发者们和 Node.js 事件模块的开发者们表示感谢!github

下面来说讲我对 JavaScript 事件的理解:面试

从上图能够看出,JavaScript 事件最核心的包括事件监听 (addListener)、事件触发 (emit)、事件删除 (removeListener)npm

事件监听(addListener)

首先,监听确定要有监听的目标,或者说是对象,那为了达到区分目标的目的,名字是不可少的,咱们定义为 type数组

其次,监听的目标必定要有某种动做,对应到 JavaScript 里实际上就是某种方法,这里定义为 fn浏览器

譬如能够监听一个 typeadd,方法为某一个变量 a 值加1的方法 fn = () => a + 1的事件。若是咱们还想监听一个使变量 b2的方法,咱们第一反应多是建立一个 typeadd2,方法 为 fn1 = () => b + 2 的事件。你可能会想,这太浪费了,我能不能只监听一个名字,让它执行多于一个方法的事件。固然是能够的。

那么怎么作呢?

很简单,把监听的方法放在一个数组里,遍历数组顺序执行就能够了。以上例子变为 typeadd,方法为[fn, fn1]

若是要细分的话还能够分为能够无限次执行的事件 on 和 只容许执行一次的事件 once (执行完后当即将事件删除)。待后详述。

事件触发(emit)

单有事件监听是不够的,必需要有事件触发才能算完成整个过程。emit 就是去触发监听的特定 type 对应的单个事件或者一系列事件。拿前面的例子来讲单个事件就是去执行 fn,一系列事件就是去遍历执行 fnfn1

事件删除(removeListener)

严格意义上来说,事件监听和事件触发已经能完成整个过程。事件删除无关紧要。但不少时候,咱们仍是须要事件删除的。好比前面讲的只容许执行一次事件 once,若是不提供删除方法,很难保证你何时会再次执行它。一般状况下,只要是再也不须要的事件,咱们都应该去删除它。

核心部分讲完,下面简单的对 eventemitter8的源码进行解析。

源码解析

所有源码:

const toString = Object.prototype.toString;
const isType = obj => toString.call(obj).slice(8, -1).toLowerCase();
const isArray = obj => Array.isArray(obj) || isType(obj) === 'array';
const isNullOrUndefined = obj => obj === null || obj === undefined;

const _addListener = function(type, fn, context, once) {
  if (typeof fn !== 'function') {
    throw new TypeError('fn must be a function');
  }

  fn.context = context;
  fn.once = !!once;

  const event = this._events[type];
  // only one, let `this._events[type]` to be a function
  if (isNullOrUndefined(event)) {
    this._events[type] = fn;
  } else if (typeof event === 'function') {
    // already has one function, `this._events[type]` must be a function before
    this._events[type] = [event, fn];
  } else if (isArray(event)) {
    // already has more than one function, just push
    this._events[type].push(fn);
  }

  return this;
};

class EventEmitter {
  constructor() {
    if (this._events === undefined) {
      this._events = Object.create(null);
    }
  }

  addListener(type, fn, context) {
    return _addListener.call(this, type, fn, context);
  }

  on(type, fn, context) {
    return this.addListener(type, fn, context);
  }

  once(type, fn, context) {
    return _addListener.call(this, type, fn, context, true);
  }

  emit(type, ...rest) {
    if (isNullOrUndefined(type)) {
      throw new Error('emit must receive at lease one argument');
    }

    const events = this._events[type];

    if (isNullOrUndefined(events)) return false;

    if (typeof events === 'function') {
      events.call(events.context || null, rest);
      if (events.once) {
        this.removeListener(type, events);
      }
    } else if (isArray(events)) {
      events.map(e => {
        e.call(e.context || null, rest);
        if (e.once) {
          this.removeListener(type, e);
        }
      });
    }

    return true;
  }

  removeListener(type, fn) {
    if (isNullOrUndefined(this._events)) return this;

    // if type is undefined or null, nothing to do, just return this
    if (isNullOrUndefined(type)) return this;

    if (typeof fn !== 'function') {
      throw new Error('fn must be a function');
    }

    const events = this._events[type];

    if (typeof events === 'function') {
      events === fn && delete this._events[type];
    } else {
      const findIndex = events.findIndex(e => e === fn);

      if (findIndex === -1) return this;

      // match the first one, shift faster than splice
      if (findIndex === 0) {
        events.shift();
      } else {
        events.splice(findIndex, 1);
      }

      // just left one listener, change Array to Function
      if (events.length === 1) {
        this._events[type] = events[0];
      }
    }

    return this;
  }

  removeAllListeners(type) {
    if (isNullOrUndefined(this._events)) return this;

    // if not provide type, remove all
    if (isNullOrUndefined(type)) this._events = Object.create(null);

    const events = this._events[type];
    if (!isNullOrUndefined(events)) {
      // check if `type` is the last one
      if (Object.keys(this._events).length === 1) {
        this._events = Object.create(null);
      } else {
        delete this._events[type];
      }
    }

    return this;
  }

  listeners(type) {
    if (isNullOrUndefined(this._events)) return [];

    const events = this._events[type];
    // use `map` because we need to return a new array
    return isNullOrUndefined(events) ? [] : (typeof events === 'function' ? [events] : events.map(o => o));
  }

  listenerCount(type) {
    if (isNullOrUndefined(this._events)) return 0;

    const events = this._events[type];

    return isNullOrUndefined(events) ? 0 : (typeof events === 'function' ? 1 : events.length);
  }

  eventNames() {
    if (isNullOrUndefined(this._events)) return [];

    return Object.keys(this._events);
  }
}

export default EventEmitter;
复制代码

代码不多,只有151行,由于写的简单版,且用的 ES6,因此才这么少;Node.js的事件和 eventemitter3可比这多且复杂很多,有兴趣可自行深刻研究。

const toString = Object.prototype.toString;
const isType = obj => toString.call(obj).slice(8, -1).toLowerCase();
const isArray = obj => Array.isArray(obj) || isType(obj) === 'array';
const isNullOrUndefined = obj => obj === null || obj === undefined;
复制代码

这4行就是一些工具函数,判断所属类型、判断是不是 null 或者 undefined

constructor() {
  if (isNullOrUndefined(this._events)) {
    this._events = Object.create(null);
  }
}
复制代码

建立了一个 EventEmitter 类,而后在构造函数里初始化一个类的 _events 属性,这个属性不须要要继承任何东西,因此用了 Object.create(null)。固然这里 isNullOrUndefined(this._events) 还去判断了一下 this._events 是否为 undefined 或者 null,若是是才须要建立。但这不是必要的,由于实例化一个 EventEmitter 都会调用构造函数,皆为初始状态,this._events 应该是不可能已经定义了的,可去掉。

addListener(type, fn, context) {
  return _addListener.call(this, type, fn, context);
}

on(type, fn, context) {
  return this.addListener(type, fn, context);
}

once(type, fn, context) {
  return _addListener.call(this, type, fn, context, true);
}
复制代码

接下来是三个方法 addListenerononce ,其中 onaddListener 的别名,可执行屡次。once 只能执行一次。

三个方法都用到了 _addListener 方法:

const _addListener = function(type, fn, context, once) {
  if (typeof fn !== 'function') {
    throw new TypeError('fn must be a function');
  }

  fn.context = context;
  fn.once = !!once;

  const event = this._events[type];
  // only one, let `this._events[type]` to be a function
  if (isNullOrUndefined(event)) {
    this._events[type] = fn;
  } else if (typeof event === 'function') {
    // already has one function, `this._events[type]` must be a function before
    this._events[type] = [event, fn];
  } else if (isArray(event)) {
    // already has more than one function, just push
    this._events[type].push(fn);
  }

  return this;
};
复制代码

方法有四个参数,type 是监听事件的名称,fn 是监听事件对应的方法,context 俗称爸爸,改变 this 指向的,也就是执行的主体。once 是一个布尔型,用来标志是否只执行一次。 首先判断 fn 的类型,若是不是方法,抛出一个类型错误。fn.context = context;fn.once = !!once 把执行主体和是否执行一次做为方法的属性。const event = this._events[type] 把该对应 type 的全部已经监听的方法存到变量 event

// only one, let `this._events[type]` to be a function
if (isNullOrUndefined(event)) {
  this._events[type] = fn;
} else if (typeof event === 'function') {
  // already has one function, `this._events[type]` must be a function before
  this._events[type] = [event, fn];
} else if (isArray(event)) {
  // already has more than one function, just push
  this._events[type].push(fn);
}

return this;
复制代码

若是 type 自己没有正在监放任何方法,this._events[type] = fn 直接把监听的方法 fn 赋给 type 属性 ;若是正在监听一个方法,则把要添加的 fn 和以前的方法变成一个含有2个元素的数组 [event, fn],而后再赋给 type 属性,若是正在监听超过2个方法,直接 push 便可。最后返回 this ,也就是 EventEmitter 实例自己。

简单来说不论是监听多少方法,都放到数组里是不必像上面细分。但性能较差,只有一个方法时 key: fn 的效率比 key: [fn] 要高。

再回头看看三个方法:

addListener(type, fn, context) {
  return _addListener.call(this, type, fn, context);
}

on(type, fn, context) {
  return this.addListener(type, fn, context);
}

once(type, fn, context) {
  return _addListener.call(this, type, fn, context, true);
}
复制代码

addListener 须要用 call 来改变 this 指向,指到了类的实例。once 则多传了一个标志位 true 来标志它只须要执行一次。这里你会看到我在 addListener 并无传 false 做为标志位,主要是由于我懒,但并不会影响到程序的逻辑。由于前面的 fn.once = !!once 已经能很好的处理不传值的状况。没传值 !!oncefalse

接下来说 emit

emit(type, ...rest) {
  if (isNullOrUndefined(type)) {
    throw new Error('emit must receive at lease one argument');
  }

  const events = this._events[type];

  if (isNullOrUndefined(events)) return false;

  if (typeof events === 'function') {
    events.call(events.context || null, rest);
    if (events.once) {
      this.removeListener(type, events);
    }
  } else if (isArray(events)) {
    events.map(e => {
      e.call(e.context || null, rest);
      if (e.once) {
        this.removeListener(type, e);
      }
    });
  }

  return true;
}
复制代码

事件触发须要指定具体的 type 不然直接抛出错误。这个很容易理解,你都没有指定名称,我怎么知道该去执行谁的事件。if (isNullOrUndefined(events)) return false,若是 type 对应的方法是 undefined 或者 null ,直接返回 false 。由于压根没有对应 type 的方法能够执行。而 emit 须要知道是否被成功触发。

接着判断 evnts 是否是一个方法,若是是, events.call(events.context || null, rest) 执行该方法,若是指定了执行主体,用 call 改变 this 的指向指向 events.context 主体,不然指向 null ,全局环境。对于浏览器环境来讲就是 window。差点忘了 restrest 是方法执行时的其余参数变量,能够不传,也能够为一个或多个。执行结束后判断 events.once ,若是为 true ,就用 removeListener 移除该监听事件。

若是 evnts 是数组,逻辑同样,只是须要遍历数组去执行全部的监听方法。

成功执行结束后返回 true

removeListener(type, fn) {
  if (isNullOrUndefined(this._events)) return this;

  // if type is undefined or null, nothing to do, just return this
  if (isNullOrUndefined(type)) return this;

  if (typeof fn !== 'function') {
    throw new Error('fn must be a function');
  }

  const events = this._events[type];

  if (typeof events === 'function') {
    events === fn && delete this._events[type];
  } else {
    const findIndex = events.findIndex(e => e === fn);

    if (findIndex === -1) return this;

    // match the first one, shift faster than splice
    if (findIndex === 0) {
      events.shift();
    } else {
      events.splice(findIndex, 1);
    }

    // just left one listener, change Array to Function
    if (events.length === 1) {
      this._events[type] = events[0];
    }
  }

  return this;
}
复制代码

removeListener 接收一个事件名称 type 和一个将要被移除的方法 fnif (isNullOrUndefined(this._events)) return this 这里表示若是 EventEmitter 实例自己的 _eventsnull 或者 undefined 的话,没有任何事件监听,直接返回 this

if (isNullOrUndefined(type)) return this 若是没有提供事件名称,也直接返回 this

if (typeof fn !== 'function') {
  throw new Error('fn must be a function');
}
复制代码

fn 若是不是一个方法,直接抛出错误,很好理解。

接着判断 type 对应的 events 是否是一个方法,是,而且 events === fn 说明 type 对应的方法有且仅有一个,等于咱们指定要删除的方法。这个时候 delete this._events[type] 直接删除掉 this._events 对象里 type 便可。

全部的 type 对应的方法都被移除后。想想 this._events[type] = undefineddelete this._events[type] 会有什么不一样?

差别是很大的,this._events[type] = undefined 仅仅是将 this._events 对象里的 type 属性赋值为 undefinedtype 这一属性依然占用内存空间,但其实已经没什么用了。若是这样的 type 一多,有可能形成内存泄漏。delete this._events[type] 则直接删除,不占内存空间。前者也是 Node.js 事件模块和 eventemitter3 早期实现的作法。

若是 events 是数组,这里我没有用 isArray 进行判断,而是直接用一个 else ,缘由是 this._events[type] 的输入限制在 on 或者 once 中,而它们已经限制了 this._events[type] 只能是方法组成的数组或者是一个方法,最多加上不当心或者人为赋成 undefinednull 的状况,但这个状况咱们也在前面判断过了。

由于 isArray 这个工具方法其实运行效率是不高的,为了追求一些效率,在不影响运行逻辑状况下能够不用 isArray 。并且 typeof events === 'function'typeof 判断方法也比 isArray 的效率要高,这也是为何不先判断是不是数组的缘由。用 typeof 去判断一个方法也比 Object.prototype.toSting.call(events) === '[object Function] 效率要高。但数组不能用 typeof 进行判断,由于返回的是 object, 这众所周知。虽然如此,在我面试过的不少人中,仍然有不少人不知道。。。

const findIndex = events.findIndex(e => e === fn) 此处用 ES6 的数组方法 findIndex 直接去查找 fnevents 中的索引。若是 findIndex === -1 说明咱们没有找到要删除的 fn ,直接返回 this 就好。若是 findIndex === 0 ,是数组第一个元素,shift 剔除,不然用 splice 剔除。由于 shiftsplice 效率高。

findIndex 的效率其实没有 for 循环去查找的高,因此 eventemitter8 的效率在我没有作 benchmark 以前我就知道确定会比 eventemitter3 效率要低很多。不那么追求执行效率时固然是用最懒的方式来写最爽。所谓的懒即正义。。。

最后还得判断移除 fnevents 剩余的数量,若是只有一个,基于以前要作的优化,this._events[type] = events[0] 把含有一个元素的数组变成一个方法,降维打击一下。。。

最后的最后 return this 返回自身,链式调用还能用得上。

removeAllListeners(type) {

  if (isNullOrUndefined(this._events)) return this;

  // if not provide type, remove all

  if (isNullOrUndefined(type)) this._events = Object.create(null);

  const events = this._events[type];

  if (!isNullOrUndefined(events)) {

    // check if type is the last one

    if (Object.keys(this._events).length === 1) {

      this._events = Object.create(null);

    } else {

      delete this._events[type];

    }

  }

  return this;

}
复制代码

removeAllListeners 指的是要删除一个 type 对应的全部方法。参数 type 是可选的,若是未指定 type ,默认把全部的监听事件删除,直接 this._events = Object.create(null) 操做便可,跟初始化 EventEmitter 类同样。

若是 events 既不是 null 且不是 undefined 说明有可删除的 type ,先用 Object.keys(this._events).length === 1 判断是否是最后一个 type 了,若是是,直接初始化 this._events = Object.create(null),不然 delete this._events[type] 直接删除 type 属性,一步到位。

最后返回 this

到目前为止,全部的核心功能已经讲完。

listeners(type) {
  if (isNullOrUndefined(this._events)) return [];

  const events = this._events[type];
  // use `map` because we need to return a new array
  return isNullOrUndefined(events) ? [] : (typeof events === 'function' ? [events] : events.map(o => o));
}

listenerCount(type) {
  if (isNullOrUndefined(this._events)) return 0;

  const events = this._events[type];

  return isNullOrUndefined(events) ? 0 : (typeof events === 'function' ? 1 : events.length);
}

eventNames() {
  if (isNullOrUndefined(this._events)) return [];

  return Object.keys(this._events);
}
复制代码

listeners 返回的是 type 对应的全部方法。结果都是一个数组,若是没有,返回空数组;若是只有一个,把它的方法放到一个数组中返回;若是原本就是一个数组,map 返回。之因此用 map 返回而不是直接 return this._events[type] 是由于 map 返回一个新的数组,是深度复制,修改数组中的值不会影响到原数组。this._events[type] 则返回原数组的一个引用,是浅度复制,稍不当心改变值会影响到原数组。形成这个差别的底层缘由是数组是一个引用类型,浅度复制只是指针拷贝。这能够单独写一篇文章,不展开了。

listenerCount 返回的是 type 对应的方法的个数,代码一眼就明白,很少说。

eventNames 这个返回的是全部 type 组成的数组,没有返回空数组,不然用 Object.keys(this._events) 直接返回。

最后的最后,export default EventEmitterEventEmitter 导出。

结语

我是先看了两个库才知道怎么写的,其实最好的学习方法是知道 EventEmitter 是干什么用的之后本身动手写,写完之后再和那些库进行对比,找出差距,修正再修正。

但也不是说先看再写没有收获,至少比只看不写和看都没看的有收获不是。。。

水平有限,代码错漏或者文章讲不清楚之处在所不免,欢迎你们批评指正。

相关文章
相关标签/搜索