系列文章: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 的博客 歡迎來訪 ^_^