经由过程源码剖析 Node.js 中 events 模块里的优化小细节

之前的文章里有说,在 Node.js 中,流(stream)是许许多多原生对象的父类,角色可谓异常重要。然则,当我们沿着“族谱”往上看时,会发明 EventEmitter 类是流(stream)类的父类,所以能够说,EventEmitter 类是 Node.js 的基本类之一,职位可显平常。虽然 EventEmitter 类暴露的接口并不多而且异常简朴,而且是少数纯 JavaScript 完成的模块之一,但由于它的运用实在是太普遍,身份太基本,所以在它的完成里到处闪光着一些优化代码实行效力,和保证极度状况下代码效果正确性的小细节。在相识以后,我们也能够将其运用到我们的一样平常编码以后,学以致用。

好,如今就让我们追随 Node.js 项目中的 lib/events.js 中的代码,来一一相识:

  • 效力更高的 键 / 值 对存储对象的建立。

  • 效力更高的从数组中去除一个元素。

  • 效力更高的不定参数的函数挪用。

  • 假如防备在一个事宜监听器中监听同一个事宜,接而致使死循环?

  • emitter.once 是怎样办到的?

效力更高的 键 / 值 对存储对象的建立

EventEmitter 类中,以 键 / 值 对的体式格局来存储事宜名和对应的监听器。在 Node.js里 ,最简朴的 键 / 值 对的存储体式格局就是直接建立一个空对象:

let store = {}
store.key = 'value'

你能够会说,ES2015 中的 Map 已在现在版本的 Node.js 中可用了,在语义上它更有上风:

let store = new Map()
store.set('key', 'value')

不过,你能够只需要一个地道的 键 / 值 对存储对象,并不需要 ObjectMap 这两个类的原型中的供应的那些过剩的要领,所以你直接:

let store = Object.create(null)
store.key = 'value'

好,我们已做的挺极致了,但这还不是 EventEmitter 中的终究完成,它的方法是运用一个空的组织函数,而且把这个组织的原型事前置空:

function Store () {}
Store.prototype = Object.create(null)

然后:

let store = new Store()
store.key = 'value'

如今让我们来比一比效力,代码:

/* global suite bench */
'use strict'

suite('key / value store', function () {
  function Store () {}
  Store.prototype = Object.create(null)

  bench('let store = {}', function () {
    let store = {}
    store.key = 'value'
  })

  bench('let store = new Map()', function () {
    let store = new Map()
    store.set('key', 'value')
  })

  bench('let store = Object.create(null)', function () {
    let store = Object.create(null)
    store.key = 'value'
  })

  bench('EventEmitter way', function () {
    let store = new Store()
    store.key = 'value'
  })
})

比较效果:

                      key / value store
      83,196,978 op/s » let store = {}
       4,826,143 op/s » let store = new Map()
       7,405,904 op/s » let store = Object.create(null)
     165,608,103 op/s » EventEmitter way

效力更高的从数组中去除一个元素

EventEmitter#removeListener 这个 API 的完成里,需要从存储的监听器数组中撤除一个元素,我们起首想到的就是运用 Array#splice 这个 API ,即 arr.splice(i, 1) 。不过这个 API 所供应的功用过于多了,它支撑去除自定义数目的元素,还支撑向数组中增加自定义的元素。所以,源码中挑选本身完成一个最小可用的:

// lib/events.js
// ...

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

比一比,代码:

/* global suite bench */
'use strict'

suite('Remove one element from an array', function () {
  function spliceOne (list, index) {
    for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) {
      list[i] = list[k]
    }
    list.pop()
  }

  bench('Array#splice', function () {
    let array = [1, 2, 3]
    array.splice(1, 1)
  })

  bench('EventEmitter way', function () {
    let array = [1, 2, 3]
    spliceOne(array, 1)
  })
})

效果,好吧,秒了:

                      Remove one element from an array
       4,262,168 op/s » Array#splice
      54,829,749 op/s » EventEmitter way

效力更高的不定参数的函数挪用

在事宜触发时,监听器具有的参数数目是恣意的,所以源码中优化了不定参数的函数挪用。

不过好吧,这里运用的是笨方法,即…把不定参数的函数挪用转变成牢固参数的函数挪用,且最多支撑到三个参数:

// lib/events.js
// ...

function emitNone(handler, isFn, self) {
  // ...
}
function emitOne(handler, isFn, self, arg1) {
  // ...
}
function emitTwo(handler, isFn, self, arg1, arg2) {
  // ...
}
function emitThree(handler, isFn, self, arg1, arg2, arg3) {
  // ...
}

function emitMany(handler, isFn, self, args) {
  // ...
}

虽然效果显而易见,我们照样比较下会差若干,以三个参数为例:

/* global suite bench */
'use strict'

suite('calling function with any amount of arguments', function () {
  function nope () {}

  bench('Function#apply', function () {
    function callMany () { nope.apply(null, arguments) }
    callMany(1, 2, 3)
  })

  bench('EventEmitter way', function () {
    function callThree (a, b, c) { nope.call(null, a, b, c) }
    callThree(1, 2, 3)
  })
})

效果显现差了一倍:

                      calling function with any amount of arguments
      11,354,996 op/s » Function#apply
      23,773,458 op/s » EventEmitter way

假如防备在一个事宜监听器中监听同一个事宜,接而致使死循环?

在注册事宜监听器时,你能否曾想到过这类状况:

'use strict'
const EventEmitter = require('events')

let myEventEmitter = new EventEmitter()

myEventEmitter.on('wtf', function wtf () {
  myEventEmitter.on('wtf', wtf)
})

myEventEmitter.emit('wtf')

运转上述代码,是不是会直接致使死循环?答案是不会,由于源码中做了处置惩罚。

我们先看一下详细的代码:

// lib/events.js
// ...

function emitMany(handler, isFn, self, args) {
  if (isFn)
    handler.apply(self, args);
  else {
    var len = handler.length;
    var listeners = arrayClone(handler, len);
    for (var i = 0; i < len; ++i)
      listeners[i].apply(self, args);
  }
}

// ...
function arrayClone(arr, i) {
  var copy = new Array(i);
  while (i--)
    copy[i] = arr[i];
  return copy;
}

个中的 handler 就是详细的事宜监听器数组,不难看出,源码中的解决方案是,运用 arrayClone 要领,拷贝出另一个如出一辙的数组,来实行它,这样一来,当我们在监听器内监听同一个事宜时,确实给原监听器数组增加了新的函数,但并没有影响到当前这个被拷贝出来的副本数组。

emitter.once 是怎样办到的

这个很简朴,运用了闭包:

function _onceWrap(target, type, listener) {
  var fired = false;
  function g() {
    target.removeListener(type, g);
    if (!fired) {
      fired = true;
      listener.apply(target, arguments);
    }
  }
  g.listener = listener;
  return g;
}

你能够会问,我既然已在 g 函数中的第一行中移除了当前的监听器,为什么还要运用 fired 这个 flag ?我个人以为是由于,在 removeListener 这个同步要领中,会将这个 g 函数暴露出来给 removeListener 事宜的监听器,所以该 flag 用来保证 once 注册的函数只会被挪用一次。

末了

剖析就到这里啦,在相识了这些做法以后,在以后我们写一些有机能请求的底层东西库等东西时,我们便能够用上它们啦。EventEmitter 类的源码并不庞杂,而且是纯 JavaScript 完成的,所以也异常引荐人人闲时一读。

参考:https://github.com/nodejs/node/blob/master/lib/events.js

    原文作者:菜菜蔡伟
    原文地址: https://segmentfault.com/a/1190000005004884
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞