系列文章:javascript
今天阅读的模块是 ee-first,经过它咱们能够在监听一系列事件时,得知哪个事件最早发生并进行相应的操做,当前包版本为 1.1.1,周下载量约为 430 万。html
首先简单介绍一下 ee-first
中的 ee ,它是 EventEmitter
的缩写,也就是事件发生器的意思,Node.js 中很多对象都继承自它,例如:net.Server
| fs.ReadStram
| stream
等,能够说许多核心 API 都是经过 EventEmitter
来进行事件驱动的,它的使用十分简单,主要是 emit
(发出事件)和 on
(监听事件) 两个接口:java
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
的用法:git
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()
判断参数是否为数组,若不是则经过抛出异常给出提示信息 —— 对于第三方模块而言,须要对调用者保持不信任的态度,因此对参数的校验十分重要。github
在早些年的时候,JavaScript 还不支持 Array.isArray()
方法,当时是经过 Object.prototype.toString.call( someVar ) === '[object Array]'
来判断 someVar
是否为数组。固然如今已是 2018 年了,已经不须要使用这些技巧。express
// 源码 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
生成一个事件监听函数:npm
// 源码 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)
}
}
复制代码
这里有两个须要注意的地方:redux
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)
这两句话:segmentfault
// 源码 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
:api
在源码 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
这个经常使用概念的具体含义。
// 源码 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
就是将一段代码经过函数包裹起来,从而延迟它的执行(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 的博客 欢迎来访 ^_^