系列文章:
- 每天閱讀一個 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
就是將一段程式碼通過函式包裹起來,從而延遲它的執行(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 的部落格 歡迎來訪 ^_^