• Node.js精进(4)——事件触发器


      Events 是 Node.js 中最重要的核心模块之一,很多模块都是依赖其创建的,例如上一节分析的流,文件、网络等模块。

      比较知名的 Express、KOA 等框架在其内部也使用了 Events 模块。

      Events 模块提供了EventEmitter类,EventEmitter 也叫事件触发器,是一种观察者模式的实现。

      观察者模式是软件设计模式的一种,在此模式中,一个目标对象(即被观察者对象)管理所有依赖于它的观察者对象。

      当其自身状态发生变化时,将以广播的方式主动发送通知(在通知中可携带一些数据),这样就能在两者之间建立触发机制,达到解耦地目的。

      与浏览器中的事件处理器不同,在 Node.js 中没有捕获、冒泡、preventDefault() 等概念或方法。

      本系列所有的示例源码都已上传至Github,点击此处获取。

    一、方法原理

      在下面的示例中,加载 events 模块,实例化 EventEmitter 类,赋值给 demo 变量,声明 listener() 监听函数。

      然后调用 demo 的 on() 方法注册 begin 事件,最后调用 emit() 触发 begin 事件,在控制台打印出“strick”。

    const EventEmitter = require('events');
    const demo = new EventEmitter();
    const listener = () => {    // 监听函数
      console.log('strick');
    };
    // 注册
    demo.on('begin', listener);
    demo.emit('begin');

      若要移除监听函数,可以像下面这样,注意,off() 方法不是移除事件,而是函数。

    demo.off('begin', listener);

    1)构造函数

      在src/lib/events.js文件中,可以看到构造函数的源码,它会调用 init() 方法,并指定 this,也就是当前实例。

    function EventEmitter(opts) {
      EventEmitter.init.call(this, opts);
    }

      删减了 init() 方法源码,只列出了关键部分,当 _events 私有属性不存在时,就通过 ObjectCreate(null) 创建。

      之所以使用 ObjectCreate(null) 是为了得到一个不继承任何原型方法的干净键值对。_events 的 key 是事件名称,value 是监听函数。

    EventEmitter.init = function(opts) {
      // 当 _events 私有属性不存在时
      if (this._events === undefined ||
          this._events === ObjectGetPrototypeOf(this)._events) {
        this._events = ObjectCreate(null);  // 不继承任何原型方法的干净键值对
        this._eventsCount = 0;
      }
    };

    2)on()

      on() 其实是 addListener() 的别名,具体逻辑在 _addListener() 函数中。

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

      在 _addListener() 函数中,会对传入的事件判断之前是否注册过。

      如果之前未注册过,那么就在键值对中注册新的事件和监听函数。

      如果之前已注册过,那么就将多个监听函数合并成数组使用,在触发时会依次执行。

      EventEmitter 默认的事件最大监听数是 10,若注册的数量超出了这个限制,那么就会发出警告,不过事件仍然可以正常触发。

    function _addListener(target, type, listener, prepend) {
      let m;
      let events;
      let existing;
      events = target._events;
      // 判断传入的事件是否注册过
      if (events === undefined) {
        events = target._events = ObjectCreate(null);
        target._eventsCount = 0;
      } else {
        existing = events[type];
      }
      // 在键值对中注册新的事件和监听函数
      if (existing === undefined) {
        events[type] = listener;
        ++target._eventsCount;
      } else {    // 已存在相同名称的事件
        // 添加第二个相同名称的事件时,将 events[type] 修改成数组
        if (typeof existing === "function") {
          existing = events[type] = prepend
            ? [listener, existing]
            : [existing, listener];
        } else if (prepend) {
          existing.unshift(listener);
        } else {
          // 若是数组,就添加到末尾
          existing.push(listener);
        }
        // 读取最大事件监听数
        m = _getMaxListeners(target);
        if (m > 0 && existing.length > m && !existing.warned) {
          existing.warned = true;
          const w = genericNodeError(
            `Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` +
            `added to ${inspect(target, { depth: -1 })}. Use emitter.setMaxListeners() to increase limit`,
            { name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length });
          process.emitWarning(w);
        }
      }
      return target;
    }

      在下面这个示例中,同一个事件,注册了两个监听函数,在触发时,会先打印“strick”,再打印“freedom”。

    const EventEmitter = require('events');
    const demo = new EventEmitter();
    const listener1 = () => {    // 监听函数
      console.log('strick');
    };
    const listener2 = () => {    // 监听函数
      console.log('freedom');
    };
    // 注册
    demo.on('begin', listener1);
    demo.on('begin', listener2);
    demo.emit('begin');

      EventEmitter 还提供了一个 once() 方法,也是用于注册事件,但只会触发一次。

    3)off()

      off() 方法是 removeListener() 的别名。

    EventEmitter.prototype.off = EventEmitter.prototype.removeListener;

      下面是删减过的 removeListener() 方法源码,先是读取指定事件的监听函数赋值给 list 变量,类型是函数或数组。

      如果要移除的事件与 list 匹配,当只剩下一个事件时,就赋值 ObjectCreate(null);否则使用 delete 关键字删除键值对的属性。

      如果 list 是一个数组时,就遍历它,并记录匹配位置。若匹配位置在头部,就调用 shift() 方法移除,否则使用 splice() 方法。

    EventEmitter.prototype.removeListener = function removeListener(type, listener) {
      const events = this._events;
      // 读取指定事件的监听函数,类型是函数或数组
      const list = events[type];
      // 要移除的事件与 list 匹配
      if (list === listener || list.listener === listener) {
          // 只剩下最后一个事件,就赋值 ObjectCreate(null)
        if (--this._eventsCount === 0) this._events = ObjectCreate(null);
        else {
          delete events[type];  // 删除键值对的属性
        }
      } else if (typeof list !== "function") {
        let position = -1;
        // 遍历 list 数组,若查到匹配的就记录位置
        for (let i = list.length - 1; i >= 0; i--) {
          if (list[i] === listener || list[i].listener === listener) {
            position = i;
            break;
          }
        }
        // 在头部就直接调用 shift() 方法
        if (position === 0) list.shift();
        else {
          if (spliceOne === undefined)
            spliceOne = require("internal/util").spliceOne;
          // 没有使用 splice() 方法,选择了一个最小可用的函数
          spliceOne(list, position);
        }
      }
      return this;
    };

      Node.js 没有使用 splice() 方法,而是选择了一个最小可用的函数,据说性能有所提升。

      spliceOne() 函数很简单,如下所示,从指定索引加一的位置开始循环,后一个元素向前搬移到上一个元素的位置,再将最后那个元素移除。

    function spliceOne(list, index) {
      for (; index + 1 < list.length; index++)
        list[index] = list[index + 1];
      list.pop();
    }

    4)emit()

      下面是删减过的 emit() 方法源码,首先读取监听函数并赋值给 handler。

      若 handler 是函数,则直接通过 apply() 运行。

      若 handler 是数组,那么先调用 arrayClone() 函数将其克隆,在遍历数组,依次通过 apply() 运行。

    EventEmitter.prototype.emit = function emit(type, ...args) {
      const handler = events[type];
      // 若 handler 是函数,则直接运行
      if (typeof handler === 'function') {
        handler.apply(this, args);
      } else {
        const len = handler.length;
        // 数组克隆,防止在 emit 时移除事件对其进行干扰
        const listeners = arrayClone(handler);
        // 遍历数组
        for (let i = 0; i < len; ++i) {
          listeners[i].apply(this, args);
        }
      }
      return true;
    };

      arrayClone() 函数的作用是防止在 emit 时移除事件对其进行干扰,在函数中使用 switch 分支和数组的 slice() 方法。

      官方说从 Node 版本 8.8.3 开始,这个实现要比简单地 for 循环快。

    function arrayClone(arr) {
      // 从 V8.8.3 开始,这个实现要比简单地  for 循环快
      switch (arr.length) {
        case 2: return [arr[0], arr[1]];
        case 3: return [arr[0], arr[1], arr[2]];
        case 4: return [arr[0], arr[1], arr[2], arr[3]];
        case 5: return [arr[0], arr[1], arr[2], arr[3], arr[4]];
        case 6: return [arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]];
      }
      // array.prototype.slice
      return ArrayPrototypeSlice(arr);
    }

    二、其他概念

    1)同步

      官方明确指出 EventEmitter 是按照注册的顺序同步地调用所有监听函数,避免竞争条件和逻辑错误。

      在适当的时候,监听函数可以使用 setImmediate() 或 process.nextTick() 方法切换到异步的操作模式,如下所示。

    const EventEmitter = require('events');
    const demo = new EventEmitter();
    demo.on('async', (a, b) => {
      setImmediate(() => {
        console.log(a, b);
      });
    });
    demo.emit('async', 'a', 'b');

    2)循环

      先来看第一个循环的示例,在注册的 loop 事件中,会不断地触发 loop 事件,那么最终会报栈溢出的错误。

    const EventEmitter = require('events');
    const demo = new EventEmitter();
    const listener = () => {
      console.log('strick');
    };
    demo.on('loop', () => {
      demo.emit('loop');
      listener();
    });
    demo.emit('loop');  // 报错

      再看看第二个循环的示例,在注册的 loop 事件中,又注册了一次 loop 事件,这么处理并不会报错,因为只是多注册了一次同名事件而已。

    const listener = () => {
     console.log('strick');
    };
    demo.on('loop', () => {
      demo.on('loop', listener);
      listener();
    });
    demo.emit('loop');  // strick
    demo.emit('loop');  // strick strick

      在每次触发时,打印的数量要比上一次多一个。

    3)错误处理

      在下面这个示例中,由于没有注册 error 事件,因此只要一触发 error 事件就会抛出错误,后面的打印也不会执行。

    const EventEmitter = require('events');
    const demo = new EventEmitter();
    demo.emit('error', new Error('error'));
    console.log('strick');

      将代码做下调整,为了防止 Node.js 主线程崩溃,应该始终注册 error 事件,改造后,虽然也会报错,但是打印仍然能正常执行。

    demo.on('error', err => {
      console.error(err);
    });
    demo.emit('error', new Error('error'));
    console.log('strick');

    参考资料:

    Node.js技术栈之事件触发器 异步迭代器 

    饿了么事件异步面试题

    深入理解Node.js之Event

    Node.js事件模块 events事件模块

    EventEmitter 源码分析与简易实现

    源码分析:EventEmitter

    详解Object.create(null)

  • 相关阅读:
    c#中String跟string的“区别”<转>
    JS中判断对象是否为空
    report builder地址:http://localhost/reports
    今天开始,主攻MS Dynamics CRM
    IO负载高的来源定位
    ORACL学习笔记 之 分区表
    Linux自动删除n天前日志
    Oracle中NVL2 和NULLIF的用法
    Ubuntu学习笔记之Sqldeveloper安装
    给ubuntu的swap分区增加容量
  • 原文地址:https://www.cnblogs.com/strick/p/16241780.html
Copyright © 2020-2023  润新知