内容简介:系列文章:今天阅读的模块是ee-first,通过它我们可以在监听一系列事件时,得知哪一个事件最先发生并进行相应的操作,当前包版本为 1.1.1,周下载量约为 430 万。首先简单介绍一下
系列文章:
- 每天阅读一个 npm 模块(1)- username
- 每天阅读一个 npm 模块(2)- mem
- 每天阅读一个 npm 模块(3)- mimic-fn
- 每天阅读一个 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 = 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}
替代不也能实现同样的移除功能嘛?
后来通过阅读作者所写的测试代码才发了在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
就是将一段代码通过函数包裹起来,从而延迟它的执行(Athunk 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 ? 。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Nginx源码阅读笔记-事件处理模块
- 每天阅读一个 npm 模块(2)- mem
- 每天阅读一个 npm 模块(1)- username
- 每天阅读一个 npm 模块(6)- pify
- # 每天阅读一个 npm 模块(7)- delegates
- 《WebKit技术内幕》阅读摘要 —— WebKit 架构和模块
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。