一句话介绍
今天阅读的模块是 https://www.npmjs.com/package/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 = this
- var err = event === 'error' ? arg1 : null
- // copy args to prevent arguments escaping scope
- for (var i = 0; i < args.length; i++) {
- args[i] = arguments[i]
- }
- done(err, ee, event, args)
- }
- }
复制代码
这里有两个需要注意的地方:
对 error 事件进行了特殊的处理, 因为在 Node.js 中, 假如进行某些操作失败了的话, 那么会将错误信息作为第一个参数传给回调函数, 例如文件的读取操作:
fs.readFile(filePath, (err, data) => { ... }
. 在我看来, 这种将错误信息作为第一个参数传给回调函数的做法, 能够引起开发者对异常信息的重视, 是十分值得推荐的编码规范.
通过 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 event
- ee.on(event, fn)
- // push this listener to the list of cleanups
- cleanups.push({
- ee: ee,
- event: event,
- fn: fn
- })
- }
- }
- function callback () {
- cleanup()
- done.apply(null, arguments)
- }
- // ...
- }
复制代码
移除响应函数
在上一步中, 不知道有没有大家注意到两个 cleanup :
在源码 5-3 的开头, 声明了 cleanups 这个数组, 并在每一次绑定响应函数的时候, 都通过 cleanups.push() 的方式, 将事件和响应函数一一对应地存储了起来.
源码 5-3 尾部的 callback 函数中, 在执行 done() 这个响应函数之前, 会调用 cleanup() 函数, 该函数十分简单, 就是通过遍历 cleanups 数组, 将之前绑定的事件监听函数再逐一移除. 之所以需要清除是因为绑定事件监听函数会对内存有不小的消耗 (这也是为什么在 Node.js 中, 默认情况下每一个 EventEmitter 最多只能绑定 10 个监听函数), 其实现如下:
- // 源码 5-4
- function cleanup () {
- var x
- for (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 = cleanup
- return thunk
- }
复制代码
thunk.cancel = cleanup 这行很容易理解, 就是让 first() 的返回值拥有移除所有响应函数的能力. 关键在于这里 thunk 函数的声明我一开始不能理解它的作用: 用 const thunk = {calcel: cleanup} 替代不也能实现同样的移除功能嘛?
后来通过阅读作者所写的测试代码才发了在 http://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 函数的含义和用法 http://www.ruanyifeng.com/blog/2015/05/thunk.html 一文中所做的定义, 我觉得非常准确, 也非常易于理解:
在 JavaScript 语言中, Thunk 函数将多参数函数替换成单参数的版本, 且只接受回调函数作为参数.
当然, 更广义地而言, 所谓 thunk 就是将一段代码通过函数包裹起来, 从而延迟它的执行 (A thunk https://en.wikipedia.org/wiki/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 ? https://github.com/reduxjs/redux-thunk#whats-a-thunk .
来源: https://juejin.im/post/5b8a7969f265da432008aa7a