天天閱讀一個 npm 模塊(5)- ee-first

系列文章:javascript

  1. 天天閱讀一個 npm 模塊(1)- username
  2. 天天閱讀一個 npm 模塊(2)- mem
  3. 天天閱讀一個 npm 模塊(3)- mimic-fn
  4. 天天閱讀一個 npm 模塊(4)- throttle-debounce

一句話介紹

今天閱讀的模塊是 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

  1. error 事件進行了特殊的處理,由於在 Node.js 中,假如進行某些操做失敗了的話,那麼會將錯誤信息做爲第一個參數傳給回調函數,例如文件的讀取操做:fs.readFile(filePath, (err, data) => { ... }。在我看來,這種將錯誤信息做爲第一個參數傳給回調函數的作法,可以引發開發者對異常信息的重視,是十分值得推薦的編碼規範。
  2. 經過 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)
  }
  
  // ...
}
複製代碼

移除響應函數

在上一步中,不知道有沒有你們注意到兩個 cleanupapi

  1. 在源碼 5-3 的開頭,聲明瞭 cleanups 這個數組,並在每一次綁定響應函數的時候,都經過 cleanups.push() 的方式,將事件和響應函數一一對應地存儲了起來。

  2. 源碼 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 的博客 歡迎來訪 ^_^

相關文章
相關標籤/搜索