系列文章:

  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. learning to rank_排序
  2. 腾讯云安装mysql本地主机名,腾讯云服务器上安装mysql,并用navicat连接
  3. 赫夫曼编码c++中的实现
  4. 基于 Android 6.0 的 小米 MIUI 8 已开源
  5. ansible 容器部署_如何使用Ansible Container管理Linux容器
  6. RoterOS负载均衡教程
  7. mysql怎么拿到一个表里的所有列名字
  8. 27个澳洲年轻人,重演了少年马云的一段奇遇
  9. jQuery知识(转)
  10. (转载)Android开发在路上:少去踩坑,多走捷径
  11. 编程语言之父6大经典名言,C语言之父这一段代码你见过吗?
  12. 计算机术语BOOTP,bootp是什么意思?
  13. swift subscript scraps
  14. Hive误删除后,如何恢复数据
  15. EasyX的安装与使用
  16. html5另存为本地文件,javascript实现文件另存为(web api)
  17. 文本摘要生成评价指标——rouge
  18. Java String.contains()方法
  19. 3D游戏建模师职业现状:大学生毕业就是失业,真的这么可怕吗
  20. 使用代码操作Excel文件(easyExcel)

热门文章

  1. Yolov4学习笔记
  2. 【ES实战】Search的滚动查询(Scroll)
  3. 运用requests模块爬取NCBI数据库论文题目及摘要
  4. div section和article区别
  5. 一个简单的FastAPI入门项目
  6. git checkout 命令
  7. uni-app应用内跳转至app-store
  8. “诺基金”成功举办2019年联糖日活动,照亮糖尿病防控道路
  9. 练——C语言练习蚂蚁感冒
  10. python爬取微博用户关注_Paython微博根据用户名搜索爬取该用户userId