系列文章:

  1. 每天阅读一个 npm 模块(1)- username
  2. 每天阅读一个 npm 模块(2)- mem
  3. 每天阅读一个 npm 模块(3)- mimic-fn
  4. 每天阅读一个 npm 模块(4)- throttle-debounce

一句话介绍

今天阅读的模块是 ee-first,通过它我们可以在监听一系列事件时,得知哪一个事件最先发生并进行相应的操作,当前包版本为 1.1.1,周下载量约为 430 万。

用法

首先简单介绍一下 ee-first 中的 ee ,它是 EventEmitter 的缩写,也就是事件发生器的意思,Node.js 中不少对象都继承自它,例如:net.Server | fs.ReadStram | stream 等,可以说许多核心 API 都是通过 EventEmitter 来进行事件驱动的,它的使用十分简单,主要是 emit (发出事件)和 on(监听事件) 两个接口:

const EventEmitter = require('events');
const emitter = new EventEmitter();emitter.on('sayHi', (name) => {console.log(`hi, my name is ${name}!`);
});emitter.emit('sayHi', 'Elvin');
// => 'hi, my name is Elvin!'
复制代码

接下来看看 ee-frist 的用法:

const EventEmitter = require('events');
const first = require('ee-first');// 1. 监听第一个发生的事件
const ee1 = new EventEmitter();
const ee2 = new EventEmitter();first([[ee1, 'close', 'end', 'error'],[ee2, 'error']
], function (err, ee, event, args) {console.log(`'${event}' happened!`);
})ee1.emit('end');
// => 'end' happened!// 2. 取消绑定的监听事件
const ee3 = new EventEmitter();
const ee4 = new EventEmitter();const trunk = first([[ee3, 'close', 'end', 'error'],[ee4, 'error']
], function (err, ee, event, args) {console.log(`'${event}' happened!`);
})trunk.cancel();
ee1.emit('end');
// => 什么都不会输出复制代码

源码学习

参数校验

源码中对参数的校验主要是通过 Array.isArray() 判断参数是否为数组,若不是则通过抛出异常给出提示信息 —— 对于第三方模块而言,需要对调用者保持不信任的态度,所以对参数的校验十分重要。

在早些年的时候,JavaScript 还不支持 Array.isArray() 方法,当时是通过 Object.prototype.toString.call( someVar ) === '[object Array]' 来判断 someVar 是否为数组。当然现在已经是 2018 年了,已经不需要使用这些技巧。

// 源码 5-1
function first (stuff, done) {if (!Array.isArray(stuff)) {throw new TypeError('arg must be an array of [ee, events...] arrays')}for (var i = 0; i < stuff.length; i++) {var arr = stuff[i]if (!Array.isArray(arr) || arr.length < 2) {throw new TypeError('each array member must be [ee, events...]')}// ...}
}
复制代码

生成响应函数

ee-first 中,首先会对传入的每一个事件名,都会通过 listener 生成一个事件监听函数:

// 源码 5-2/*** Create the event listener.* * @param  {String}    event, 事件名,例如 'end', 'error' 等* @param  {Function}  done, 调用 ee-first 时传入的响应函数*/
function listener (event, done) {return function onevent (arg1) {var args = new Array(arguments.length)var ee = thisvar err = event === 'error' ? arg1 : null// copy args to prevent arguments escaping scopefor (var i = 0; i < args.length; i++) {args[i] = arguments[i]}done(err, ee, event, args)}
}
复制代码

这里有两个需要注意的地方:

  1. error 事件进行了特殊的处理,因为在 Node.js 中,假如进行某些操作失败了的话,那么会将错误信息作为第一个参数传给回调函数,例如文件的读取操作:fs.readFile(filePath, (err, data) => { ... }。在我看来,这种将错误信息作为第一个参数传给回调函数的做法,能够引起开发者对异常信息的重视,是十分值得推荐的编码规范。
  2. 通过 new Array() 和循环赋值的操作,将 onevent 函数的参数保存在了新数组 args 中,并将其传递给 done 函数。假如不考虑低版本兼容性的话,这里可以使用 ES6 的方法 Array.from() 实现这个功能。不过我暂时没有想出为什么要进行这个复制操作,虽然作者进行了注释,说是为了防止参数作用域异常,但是我没有想到这个场景,希望知道的读者能在评论区指出来~

绑定响应函数

接下来则是将生成的事件响应函数绑定到对应的 EventEmitter 上即可,关键就是 var fn = listener(event, callback); ee.on(event, fn) 这两句话:

// 源码 5-3
function first (stuff, done) {var cleanups = []for (var i = 0; i < stuff.length; i++) {var arr = stuff[i]var ee = arr[0]for (var j = 1; j < arr.length; j++) {var event = arr[j]var fn = listener(event, callback)// listen to the eventee.on(event, fn)// push this listener to the list of cleanupscleanups.push({ee: ee,event: event,fn: fn})}}function callback () {cleanup()done.apply(null, arguments)}// ...
}
复制代码

移除响应函数

在上一步中,不知道有没有大家注意到两个 cleanup

  1. 在源码 5-3 的开头,声明了 cleanups 这个数组,并在每一次绑定响应函数的时候,都通过 cleanups.push() 的方式,将事件和响应函数一一对应地存储了起来。

  2. 源码 5-3 尾部的 callback 函数中,在执行 done() 这个响应函数之前,会调用 cleanup() 函数,该函数十分简单,就是通过遍历 cleanups 数组,将之前绑定的事件监听函数再逐一移除。之所以需要清除是因为绑定事件监听函数会对内存有不小的消耗(这也是为什么在 Node.js 中,默认情况下每一个 EventEmitter 最多只能绑定 10 个监听函数),其实现如下:

    // 源码 5-4
    function cleanup () {var xfor (var i = 0; i < cleanups.length; i++) {x = cleanups[i]x.ee.removeListener(x.event, x.fn)}
    }
    复制代码

thunk 函数

最后还剩下一点代码没有说到,这段代码最短,但也是让我收获最大的地方 —— 帮我理解了 thunk 这个常用概念的具体含义。

// 源码 5-5
function first (stuff, done) {// ...function thunk (fn) {done = fn}thunk.cancel = cleanupreturn thunk
}
复制代码

thunk.cancel = cleanup 这行很容易理解,就是让 first() 的返回值拥有移除所有响应函数的能力。关键在于这里 thunk 函数的声明我一开始不能理解它的作用:用 const thunk = {calcel: cleanup} 替代不也能实现同样的移除功能嘛?

后来通过阅读作者所写的测试代码才发了在 README.md 中没有提到的用法:

// 源码 5-6 测试代码
const EventEmitter = require('events').EventEmitter
const assert = require('assert')
const first = require('ee-first')it('should return a thunk', function (testDone) {const thunk = first([[ee1, 'a', 'b', 'c'],[ee2, 'a', 'b', 'c'],[ee3, 'a', 'b', 'c'],])thunk(function (err, ee, event, args) {assert.ifError(err)assert.equal(ee, ee2)assert.equal(event, 'b')assert.deepEqual(args, [1, 2, 3])testDone()})ee2.emit('b', 1, 2, 3)
})
复制代码

上面的代码很好的展示了 thunk 的作用:它将本来需要两个参数的 first(stuff, done) 函数变成了只需要一个回调函数作为参数的 thunk(done) 函数。

这里引用阮一峰老师在 Thunk 函数的含义和用法 一文中所做的定义,我觉得非常准确,也非常易于理解:

在 JavaScript 语言中,Thunk 函数将多参数函数替换成单参数的版本,且只接受回调函数作为参数

当然,更广义地而言,所谓 thunk 就是将一段代码通过函数包裹起来,从而延迟它的执行(A thunk is a function that wraps an expression to delay its evaluation)。

// 这段代码会立即执行
// x === 3
let x = 1 + 2;// 1 + 2 只有在 foo 函数被调用时才执行
// 所以 foo 就是一个 thunk
let foo = () => 1 + 2
复制代码

这段解释和示例代码来自于 redux-thunk - Whtat's a thunk ?。

写在最后

ee-first 是我这些天读过的最舒服的代码,既有详尽的注释,也不会像昨天所阅读的 throttle-debounce 模块那样让人觉得注释过于冗余。

另外当面对一段代码不知有何作用时,可以通过相关的测试代码入手进行探索。

关于我:毕业于华科,工作在腾讯,elvin 的博客 欢迎来访 ^_^

每天阅读一个 npm 模块(5)- ee-first相关推荐

  1. 每天阅读一个 npm 模块(4)- throttle-debounce

    系列文章: 每天阅读一个 npm 模块(1)- username 每天阅读一个 npm 模块(2)- mem 每天阅读一个 npm 模块(3)- mimic-fn 上一篇文章中介绍的属性描述符的知识太 ...

  2. # 每天阅读一个 npm 模块(8)- koa-route

    系列文章: 每天阅读一个 npm 模块(1)- username 每天阅读一个 npm 模块(2)- mem 每天阅读一个 npm 模块(3)- mimic-fn 每天阅读一个 npm 模块(4)- ...

  3. 每天阅读一个 npm 模块(1)- username

    最近工作比较繁忙,每天能用于学习知识的时间越来越少,深感这样不利于自己的技术提升.恰好想起 狼叔 所说的 "迷茫时学习 Node.js 最好的方法 - 每天看十个 npm 模块", ...

  4. 每天看 10 个 NPM 模块?

    最近看到阿里前端技术专家狼叔在 17 年的这篇<迷茫时学习 Node.js 最好的方法>[1]提到: 今天小弟过来找我,说迷茫,我告诉他一个密法:一天看 10 个 npm 模块,坚持一年就 ...

  5. [译] 在 Google Apps 脚本中使用 ES6 和 npm 模块

    原文地址:Using ES6 and npm modules in Google Apps Script 原文作者:Prasanth Janardanan 译文出自:掘金翻译计划 本文永久链接:git ...

  6. nodejs安装及npm模块插件安装路径配置

    在学习完js后,我们就要进入nodejs的学习,因此就必须配置nodejs和npm的属性了. 我相信,个别人在安装时会遇到这样那样的问题,看着同学都已装好,难免会焦虑起来.于是就开始上网查找解决方案, ...

  7. 如何在React Native中写一个自定义模块

    前言 在 React Native 项目中可以看到 node_modules 文件夹,这是存放 node 模块的地方,Node.js 的包管理器 npm 是全球最大的开源库生态系统.提到npm,一般指 ...

  8. 如何在Node JS中卸载NPM模块?

    本文翻译自:How to uninstall npm modules in node js? As commonly known, any npm module can be installed by ...

  9. 手撸一个npm包,安利一下duiba-sprite

    背景 我所在组负责我司线上H5互动小游戏的开发,其中一部分开发者负责皮肤的开发.大致流程为:视觉出psd,开发者切图,开发者开发,开发者上传皮肤代码,运营验收.这里边有个奇葩的动作:开发者切图,为什么 ...

最新文章

  1. CSS动画效果无限循环放大缩小
  2. 用欧几里得算法求最大公约数_欧几里得算法:GCD(最大公约数),用C ++和Java示例解释...
  3. TCP/IP / 如何保证数据包传输的有序可靠?
  4. C#修改系统环境变量,调用批处理bat
  5. Qt使用信号与槽时出现的错误“Incompatible sender/receiver arguments”
  6. python tab和空格混用_我的 Python 编码规范
  7. Oracle中NVL2 和NULLIF的用法
  8. 从源代码中加载res / values / dimension.xml中的维度值
  9. 通俗理解数字签名,数字证书和https
  10. DevExpress WinForm 控件汉化方法 代码(一)
  11. 链接装载与库:第十一章——运行库
  12. SolidWorks二次开发-工程图-预定义视图
  13. 《初等数论》:整除性概念及其性质、质数与合数
  14. 概率论中 Var是什么意思?概率论方差概念介绍
  15. ElasticSearch排序引起的all shards failed异常
  16. Word文档导出(使用固定模板)
  17. int 长度 mysql_MySQL int 类型的长度和范围解惑
  18. python大数据课程_Python课程(大数据系列)ElasticSearch从基础与实战视频课程
  19. 52ypay comsubmit php,Hack易支付平台 - 一站式免签约支付方案-Hack易支付
  20. 清华大学计算机系哪个专业就业前景最好,清华大学有哪些专业最好就业?很多人都不知道!...

热门文章

  1. 计组——定点数原码反码补码移码以及它们之间的转换
  2. SpringBoot 配置CORS
  3. 神经影像信号处理总成(EEG、SEEG、MRI、CT)
  4. GStreamer 安装
  5. 微信小程序开发之颜值测试,调用百度AI人脸检测接口
  6. APNs Auth Key Token 验证模式
  7. CSS伪类的基本使用
  8. 计算机二级班级排名公式,Excel中快速计算班级名次和年级名次,这样的方法值得借鉴...
  9. 计算机图形学头歌实训平台——三维造型
  10. 计算机 夏令营 经验,夏令营经验总结(中国石油大学克校区计算机类专业)