zoukankan      html  css  js  c++  java
  • 深入出不来nodejs源码-events模块

      这一节内容超级简单,纯JS,就当给自己放个假了,V8引擎和node的C++代码看得有点脑阔疼。

      学过DOM的应该都知道一个API,叫addeventlistener,即事件绑定。这个东西贯穿了整个JS的学习过程,无论是刚开始的自己获取DOM手动绑,还是后期vue的直接@click,所有的交互都离不开这个东西。

      同样,在node中,事件绑定也贯穿了整个框架。基本上大多数的内置模块以events为原型,下面的代码随处可见:

    EventEmitter.call(this);

      不同的是,页面上DOM的事件绑定是由浏览器来实现,触发也是一些操作'间接'触发,并不需要去主动emit对应事件,并且有冒泡和捕获这两特殊的性质。

      但是在node中,不存在dom,绑定的目标是一个对象(dom本质上也是对象),在内部node自己用纯JS实现了一个事件绑定与事件触发类。

      

      本文相关源码来源于https://github.com/nodejs/node/blob/master/lib/events.js。

      首先看一下构造函数:

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

      这里会调用一个init方法,this指向调用对象,初始化方法也很简单:

    EventEmitter.init = function() {
      // 事件属性
      if (this._events === undefined ||
          this._events === Object.getPrototypeOf(this)._events) {
        this._events = Object.create(null);
        this._eventsCount = 0;
      }
      // 同类型事件最大监听数量
      this._maxListeners = this._maxListeners || undefined;
    };

      涉及的三个属性分别是:

    1、_events => 一个挂载属性,空对象,负责收集所有类型的事件

    2、_eventsCount => 记录目前绑定事件类型的数量

    3、_maxListeners => 同类型事件listener数量限制

      事件相关的主要操作有3个,依次来看。

    绑定事件/on

      虽然一般用的AP都是event.on,但是其实用addListener是一样的:

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

      这个addListener跟DOM的addEventListener稍微有点不一样,前两个参数一致,分别代表类型、回调函数。

      但是最后一个参数,这里代表的是否优先插入该事件,有一个方法就是做这个的:

    EventEmitter.prototype.prependListener =
        function prependListener(type, listener) {
          return _addListener(this, type, listener, true);
        };

      最终都指向这个_addListener,分步解释:

    /**
     * 事件绑定方法
     * @param {Object} target 目标对象
     * @param {String} type 事件名称
     * @param {Function} listener 回调函数
     * @param {Boolean} prepend 是否插入
     */
    function _addListener(target, type, listener, prepend) {
      // 指定事件类型的回调函数数量
      var m;
      // 事件属性对象
      var events;
      // 对应类型的回调函数
      var existing;
    
      if (typeof listener !== 'function') {
        const errors = lazyErrors();
        throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener);
      }
      // 尝试获取对应类型的事件
      events = target._events;
    
      // 未找到对应的事件相关属性
      if (events === undefined) {
        events = target._events = Object.create(null);
        target._eventsCount = 0;
      }
      // 当存在对象的事件属性对象时
      else {}
    
      // more...
    
      return target;
    }

      这里首先会尝试获取指定对象的_events属性,即构造函数中初始化的挂载对象属性。

      由于无论是任意构造函数中调用EventEmitter.call(this)或者new EventEmitter()都会在生成对象上挂载一个_events对象,所以这个判断暂时找不到反例。

      当不存在就手动初始化一个,并添加一个记数属性重置为0。

      当存在时,处理代码如下:

    events = target._events;
    if (events === undefined) {
      // ...
    } else {
      // To avoid recursion in the case that type === "newListener"! Before
      // adding it to the listeners, first emit "newListener".
      if (events.newListener !== undefined) {
        target.emit('newListener', type,
                    listener.listener ? listener.listener : listener);
    
        // Re-assign `events` because a newListener handler could have caused the
        // this._events to be assigned to a new object
        events = target._events;
      }
      // 尝试获取对应类型的回调函数集合
      existing = events[type];
    }

      这个地方的注释主要讲的是,当绑定了type为newListener的事件时,每次都会触发一次这个事件,如果再次绑定该事件会出现递归问题。所以要判断是否存在newListener事件类型,如果有就先触发一次newListener事件。

      先不管这个,最后会尝试获取指定类型的事件listener容器,下面就是对existing的处理。

    // 首次添加该类型事件时
    if (existing === undefined) {
      // 直接把函数赋值给对应类型的key
      existing = events[type] = listener;
      // 记数+1
      ++target._eventsCount;
    } else {
      // 1.已有对应类型 但是只有一个
      if (typeof existing === 'function') {
        // 转换数组 根据prepend参数安排顺序
        existing = events[type] =
          prepend ? [listener, existing] : [existing, listener];
        // If we've already got an array, just append.
      }
      // 2.已有多个 判断是否有优先的flag进行前插或后插
      else if (prepend) {
        existing.unshift(listener);
      } else {
        existing.push(listener);
      }
    
      // Check for listener leak
      // ...
    }

      这里的处理就能很清楚的看到events模块对于事件绑定的处理,_events相当于一个总对象,属性的key就是对应的事件类型type,而key对应的value就是对应的listener。只有一个时,就直接用该listener做值。重复绑定同类型的事件,这时值会转换为数组保存所有的listener。这里prepend就是之前的最后一个参数,允许函数插入到队列的前面,优先触发。

      最后还有一个绑定事件的数量判断:

    // 获取_maxListeners参数 同类型事件listener最大绑定数量
    m = $getMaxListeners(target);
    // 如果超出就发出可能有内存泄漏的警告
    if (m > 0 && existing.length > m && !existing.warned) {
      existing.warned = true;
      // 因为是warning所以不会有error code 可以不理这个东西
      // eslint-disable-next-line no-restricted-syntax
      const w = new Error('Possible EventEmitter memory leak detected. ' +
                          `${existing.length} ${String(type)} listeners ` +
                          'added. Use emitter.setMaxListeners() to ' +
                          'increase limit');
      w.name = 'MaxListenersExceededWarning';
      w.emitter = target;
      w.type = type;
      w.count = existing.length;
      process.emitWarning(w);
    }

      看看就好,程序员不用管warning,哈哈。

    一次绑定事件/once

      有些时候希望事件只触发一次,原生的API目前不存在该功能,当初jquery也是封装了一个once方法,对应的这个events模块也有。

    EventEmitter.prototype.once = function once(type, listener) {
      if (typeof listener !== 'function') {
        const errors = lazyErrors();
        throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener);
      }
      this.on(type, _onceWrap(this, type, listener));
      return this;
    };

      除去那个判断,其实绑定的方法还是同一个,只是对应的listener变成了一个包装函数,来看看。

    function _onceWrap(target, type, listener) {
      // this绑定对象
      var state = { fired: false, wrapFn: undefined, target, type, listener };
      var wrapped = onceWrapper.bind(state);
      // 原生的listener挂载到这个包装函数上
      wrapped.listener = listener;
      // 处理完后更新state属性
      state.wrapFn = wrapped;
      // 返回的是一个包装的函数
      return wrapped;
    }
    
    function onceWrapper(...args) {
      // 这里所有的this指向上面的state对象
      // args来源于触发时候给的参数
      if (!this.fired) {
        // 解绑该包装后的listener
        this.target.removeListener(this.type, this.wrapFn);
        this.fired = true;
        // 触发listener
        Reflect.apply(this.listener, this.target, args);
      }
    }

      思路其实跟jquery的源码差不多,也是包装listener,当触发一次事件时,先解绑这个listener再触发事件。

      需要注意的是,这里存在两个listener,一个是原生的,一个是包装后的。绑定的是包装的,所以解绑的第二个参数也要是包装的。其中原生的作为listener属性挂载到包装后的函数上,实际上触发包装listener后内部会隐式调用原生listener。

    事件触发/emit

      看完绑定,来看触发。

    EventEmitter.prototype.emit = function emit(type, ...args) {
      let doError = (type === 'error');
    
      const events = this._events;
      // 判断是否触发的error类型事件
      if (events !== undefined)
        doError = (doError && events.error === undefined);
      else if (!doError)
        return false;
    
      // If there is no 'error' event listener then throw.
      if (doError) {
        // 错误处理 不看
      }
      // 跟之前的existing一个东西
      const handler = events[type];
    
      if (handler === undefined)
        return false;
      // 如果只有一个 直接调用
      if (typeof handler === 'function') {
        Reflect.apply(handler, this, args);
      } else {
        // 多个listener 依次触发
        const len = handler.length;
        const listeners = arrayClone(handler, len);
        for (var i = 0; i < len; ++i)
          Reflect.apply(listeners[i], this, args);
      }
    
      return true;
    };

      太简单了,懒得解释。

    事件解绑/removeListener

      同样分几步来看解绑的过程,首先是参数声明:

    // Emits a 'removeListener' event if and only if the listener was removed.
    EventEmitter.prototype.removeListener = function removeListener(type, listener) {
      // list => listener容器
      // events => 事件根对象
      // position => 记录删除listener位置
      // i => 迭代参数
      // originalListener => 原生listener 参考上面的once
      var list, events, position, i, originalListener;
    
      if (typeof listener !== 'function') {
        const errors = lazyErrors();
        throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener);
      }
    
      events = this._events;
      if (events === undefined)
        return this;
    
      list = events[type];
      if (list === undefined)
        return this;
    
      // ...
    }

      比较简单,每个参数的用处都很明显,错误判断后,下面有两种不同的情况。

      当对应type的listener只有一个时:

    EventEmitter.prototype.removeListener = function removeListener(type, listener) {
      // list => listener容器
      // events => 事件根对象
      // position => 记录删除listener位置
      // i => 迭代参数
      // originalListener => 原生listener 参考上面的once
      var list, events, position, i, originalListener;
    
      // ...
    
      // listener只有一个的情况
      if (list === listener || list.listener === listener) {
        // 如果一个绑定事件都没了 直接重置_events对象
        if (--this._eventsCount === 0)
          this._events = Object.create(null);
        else {
          // 删除对应的事件类型
          delete events[type];
          // 尝试触发一次removeListener事件
          if (events.removeListener)
            this.emit('removeListener', type, list.listener || listener);
        }
      } else if (typeof list !== 'function') {
        // ...
      }
    
      return this;
    };

      这里还分了两种情况,如果_eventsCount为0,即所有的type都被清完,会重置_events对象。

      理论上来说,按照else分支的逻辑,当listener剩一个的时候都是直接delete对应的key,最后剩下的还是一个空对象,那这里的重重置似乎变得没有意义了。

      我猜测估计是为了V8层面的优化,因为对象的属性在破坏性变动(添加属性、重复绑定同type事件导致函数变成函数数组)的时候,所需的内存会进行扩充,这个过程是不可逆的,就算最后只剩一个空壳对象,其实际占用也是相当大的。所以为了省空间,这里进行重置,用很小的空间初始化_events对象,原来的空间被回收。

      当对应type的listener为多个时,就要遍历了。

    if (list === listener || list.listener === listener) {
      // ...
    } else if (typeof list !== 'function') {
      position = -1;
      // 倒序遍历
      for (i = list.length - 1; i >= 0; i--) {
        if (list[i] === listener || list[i].listener === listener) {
          // once绑定的事件有listener属性
          originalListener = list[i].listener;
          // 记录位置
          position = i;
          break;
        }
      }
    
      if (position < 0)
        return this;
      // 在第一个位置时
      if (position === 0)
        list.shift();
      else {
        // 删除数组对应索引的值
        if (spliceOne === undefined)
          spliceOne = require('internal/util').spliceOne;
        spliceOne(list, position);
      }
      // 如果数组里只有一个值 转换为单个值
      // 有点像HashMap的链表-红黑树转换……
      if (list.length === 1)
        events[type] = list[0];
      // 尝试触发removeListener
      if (events.removeListener !== undefined)
        this.emit('removeListener', type, originalListener || listener);
    }

      太简单了,自己看吧。

      其他还有诸如removeAllListeners、_listeners、eventNames等API,有兴趣的可以自行去看。

  • 相关阅读:
    VS缓冲区溢出,未对变量进行检查
    Mutex与Event控制互斥事件的使用详解
    error LNK2019: 无法解析的外部符号
    [转] 兼容IE和Firefox的设为首页和收藏的Javascript代码
    [转]超时时间以到,但尚未从池中获取连接
    Datalist的嵌套使用
    由服务器端向客户端输出脚本
    几个国外的XHTML模板站,DIV+CSS模板下载(转)
    gridview隐藏某一列
    [转]简单谈基于SQL SERVER 分页存储过程的演进
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/9438769.html
Copyright © 2011-2022 走看看