资讯专栏INFORMATION COLUMN

通过源码解析 Node.js 中 events 模块里的优化小细节

cloud / 3476人阅读

摘要:之前的文章里有说,在中,流是许许多多原生对象的父类,角色可谓十分重要。效率更高的从数组中去除一个元素。不过这个所提供的功能过于多了,它支持去除自定义数量的元素,还支持向数组中添加自定义的元素。

之前的文章里有说,在 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://www.ucloud.cn/yun/79295.html

相关文章

  • 解析nodeJS模块源码 亲手打造基于ES6的观察者系统

    摘要:为指定事件注册一个单次监听器,即监听器最多只会触发一次,触发后立刻解除该监听器。移除指定事件的某个监听器,监听器必须是该事件已经注册过的监听器。返回指定事件的监听器数组。如何创建空对象我们已经了解到,是要来储存监听事件监听器数组的。 毫无疑问,nodeJS改变了整个前端开发生态。本文通过分析nodeJS当中events模块源码,由浅入深,动手实现了属于自己的ES6事件观察者系统。千万不...

    csRyan 评论0 收藏0
  • Vue.js 源码学习笔记

    摘要:实际上,我在看代码的过程中顺手提交了这个,作者眼明手快,当天就进行了修复,现在最新的代码里已经不是这个样子了而且状态机标识由字符串换成了数字常量,解析更准确的同时执行效率也会更高。 最近饶有兴致的又把最新版 Vue.js 的源码学习了一下,觉得真心不错,个人觉得 Vue.js 的代码非常之优雅而且精辟,作者本身可能无 (bu) 意 (xie) 提及这些。那么,就让我来吧:) 程序结构梳...

    darkbaby123 评论0 收藏0
  • Vue.js 源码学习笔记

    摘要:实际上,我在看代码的过程中顺手提交了这个,作者眼明手快,当天就进行了修复,现在最新的代码里已经不是这个样子了而且状态机标识由字符串换成了数字常量,解析更准确的同时执行效率也会更高。 最近饶有兴致的又把最新版 Vue.js 的源码学习了一下,觉得真心不错,个人觉得 Vue.js 的代码非常之优雅而且精辟,作者本身可能无 (bu) 意 (xie) 提及这些。那么,就让我来吧:) 程序结构梳...

    jsdt 评论0 收藏0
  • 【全文】狼叔:如何正确的学习Node.js

    摘要:感谢大神的免费的计算机编程类中文书籍收录并推荐地址,以后在仓库里更新地址,声音版全文狼叔如何正确的学习简介现在,越来越多的科技公司和开发者开始使用开发各种应用。 说明 2017-12-14 我发了一篇文章《没用过Node.js,就别瞎逼逼》是因为有人在知乎上黑Node.js。那篇文章的反响还是相当不错的,甚至连著名的hax贺老都很认同,下班时读那篇文章,竟然坐车的还坐过站了。大家可以很...

    Edison 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<