Events 是 Node.js 中最重要的核心模組之一,很多模組都是依賴其建立的,例如上一節分析的流,檔案、網路等模組。
比較知名的 Express、KOA 等框架在其內部也使用了 Events 模組。
Events 模組提供了EventEmitter類,EventEmitter 也叫事件觸發器,是一種觀察者模式的實現。
觀察者模式是軟體設計模式的一種,在此模式中,一個目標物件(即被觀察者物件)管理所有依賴於它的觀察者物件。
當其自身狀態發生變化時,將以廣播的方式主動傳送通知(在通知中可攜帶一些資料),這樣就能在兩者之間建立觸發機制,達到解耦地目的。
與瀏覽器中的事件處理器不同,在 Node.js 中沒有捕獲、冒泡、preventDefault() 等概念或方法。
本系列所有的範例原始碼都已上傳至Github,點選此處獲取。
在下面的範例中,載入 events 模組,範例化 EventEmitter 類,賦值給 demo 變數,宣告 listener() 監聽函數。
然後呼叫 demo 的 on() 方法註冊 begin 事件,最後呼叫 emit() 觸發 begin 事件,在控制檯列印出「strick」。
const EventEmitter = require('events'); const demo = new EventEmitter(); const listener = () => { // 監聽函數 console.log('strick'); }; // 註冊 demo.on('begin', listener); demo.emit('begin');
若要移除監聽函數,可以像下面這樣,注意,off() 方法不是移除事件,而是函數。
demo.off('begin', listener);
1)建構函式
在src/lib/events.js檔案中,可以看到建構函式的原始碼,它會呼叫 init() 方法,並指定 this,也就是當前範例。
function EventEmitter(opts) { EventEmitter.init.call(this, opts); }
刪減了 init() 方法原始碼,只列出了關鍵部分,當 _events 私有屬性不存在時,就通過 ObjectCreate(null) 建立。
之所以使用 ObjectCreate(null) 是為了得到一個不繼承任何原型方法的乾淨鍵值對。_events 的 key 是事件名稱,value 是監聽函數。
EventEmitter.init = function(opts) { // 當 _events 私有屬性不存在時 if (this._events === undefined || this._events === ObjectGetPrototypeOf(this)._events) { this._events = ObjectCreate(null); // 不繼承任何原型方法的乾淨鍵值對 this._eventsCount = 0; } };
2)on()
on() 其實是 addListener() 的別名,具體邏輯在 _addListener() 函數中。
EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener;
在 _addListener() 函數中,會對傳入的事件判斷之前是否註冊過。
如果之前未註冊過,那麼就在鍵值對中註冊新的事件和監聽函數。
如果之前已註冊過,那麼就將多個監聽函數合併成陣列使用,在觸發時會依次執行。
EventEmitter 預設的事件最大監聽數是 10,若註冊的數量超出了這個限制,那麼就會發出警告,不過事件仍然可以正常觸發。
function _addListener(target, type, listener, prepend) { let m; let events; let existing; events = target._events; // 判斷傳入的事件是否註冊過 if (events === undefined) { events = target._events = ObjectCreate(null); target._eventsCount = 0; } else { existing = events[type]; } // 在鍵值對中註冊新的事件和監聽函數 if (existing === undefined) { events[type] = listener; ++target._eventsCount; } else { // 已存在相同名稱的事件 // 新增第二個相同名稱的事件時,將 events[type] 修改成陣列 if (typeof existing === "function") { existing = events[type] = prepend ? [listener, existing] : [existing, listener]; } else if (prepend) { existing.unshift(listener); } else { // 若是陣列,就新增到末尾 existing.push(listener); } // 讀取最大事件監聽數 m = _getMaxListeners(target); if (m > 0 && existing.length > m && !existing.warned) { existing.warned = true; const w = genericNodeError( `Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` + `added to ${inspect(target, { depth: -1 })}. Use emitter.setMaxListeners() to increase limit`, { name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length }); process.emitWarning(w); } } return target; }
在下面這個範例中,同一個事件,註冊了兩個監聽函數,在觸發時,會先列印「strick」,再列印「freedom」。
const EventEmitter = require('events'); const demo = new EventEmitter(); const listener1 = () => { // 監聽函數 console.log('strick'); }; const listener2 = () => { // 監聽函數 console.log('freedom'); }; // 註冊 demo.on('begin', listener1); demo.on('begin', listener2); demo.emit('begin');
EventEmitter 還提供了一個 once() 方法,也是用於註冊事件,但只會觸發一次。
3)off()
off() 方法是 removeListener() 的別名。
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
下面是刪減過的 removeListener() 方法原始碼,先是讀取指定事件的監聽函數賦值給 list 變數,型別是函數或陣列。
如果要移除的事件與 list 匹配,當只剩下一個事件時,就賦值 ObjectCreate(null);否則使用 delete 關鍵字刪除鍵值對的屬性。
如果 list 是一個陣列時,就遍歷它,並記錄匹配位置。若匹配位置在頭部,就呼叫 shift() 方法移除,否則使用 splice() 方法。
EventEmitter.prototype.removeListener = function removeListener(type, listener) { const events = this._events; // 讀取指定事件的監聽函數,型別是函數或陣列 const list = events[type]; // 要移除的事件與 list 匹配 if (list === listener || list.listener === listener) { // 只剩下最後一個事件,就賦值 ObjectCreate(null) if (--this._eventsCount === 0) this._events = ObjectCreate(null); else { delete events[type]; // 刪除鍵值對的屬性 } } else if (typeof list !== "function") { let position = -1; // 遍歷 list 陣列,若查到匹配的就記錄位置 for (let i = list.length - 1; i >= 0; i--) { if (list[i] === listener || list[i].listener === listener) { position = i; break; } } // 在頭部就直接呼叫 shift() 方法 if (position === 0) list.shift(); else { if (spliceOne === undefined) spliceOne = require("internal/util").spliceOne; // 沒有使用 splice() 方法,選擇了一個最小可用的函數 spliceOne(list, position); } } return this; };
Node.js 沒有使用 splice() 方法,而是選擇了一個最小可用的函數,據說效能有所提升。
spliceOne() 函數很簡單,如下所示,從指定索引加一的位置開始迴圈,後一個元素向前搬移到上一個元素的位置,再將最後那個元素移除。
function spliceOne(list, index) { for (; index + 1 < list.length; index++) list[index] = list[index + 1]; list.pop(); }
4)emit()
下面是刪減過的 emit() 方法原始碼,首先讀取監聽函數並賦值給 handler。
若 handler 是函數,則直接通過 apply() 執行。
若 handler 是陣列,那麼先呼叫 arrayClone() 函數將其克隆,在遍歷陣列,依次通過 apply() 執行。
EventEmitter.prototype.emit = function emit(type, ...args) { const handler = events[type]; // 若 handler 是函數,則直接執行 if (typeof handler === 'function') { handler.apply(this, args); } else { const len = handler.length; // 陣列克隆,防止在 emit 時移除事件對其進行干擾 const listeners = arrayClone(handler); // 遍歷陣列 for (let i = 0; i < len; ++i) { listeners[i].apply(this, args); } } return true; };
arrayClone() 函數的作用是防止在 emit 時移除事件對其進行干擾,在函數中使用 switch 分支和陣列的 slice() 方法。
官方說從 Node 版本 8.8.3 開始,這個實現要比簡單地 for 迴圈快。
function arrayClone(arr) { // 從 V8.8.3 開始,這個實現要比簡單地 for 迴圈快 switch (arr.length) { case 2: return [arr[0], arr[1]]; case 3: return [arr[0], arr[1], arr[2]]; case 4: return [arr[0], arr[1], arr[2], arr[3]]; case 5: return [arr[0], arr[1], arr[2], arr[3], arr[4]]; case 6: return [arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]]; } // array.prototype.slice return ArrayPrototypeSlice(arr); }
1)同步
官方明確指出 EventEmitter 是按照註冊的順序同步地呼叫所有監聽函數,避免競爭條件和邏輯錯誤。
在適當的時候,監聽函數可以使用 setImmediate() 或 process.nextTick() 方法切換到非同步的操作模式,如下所示。
const EventEmitter = require('events'); const demo = new EventEmitter(); demo.on('async', (a, b) => { setImmediate(() => { console.log(a, b); }); }); demo.emit('async', 'a', 'b');
2)迴圈
先來看第一個迴圈的範例,在註冊的 loop 事件中,會不斷地觸發 loop 事件,那麼最終會報棧溢位的錯誤。
const EventEmitter = require('events'); const demo = new EventEmitter(); const listener = () => { console.log('strick'); }; demo.on('loop', () => { demo.emit('loop'); listener(); }); demo.emit('loop'); // 報錯
再看看第二個迴圈的範例,在註冊的 loop 事件中,又註冊了一次 loop 事件,這麼處理並不會報錯,因為只是多註冊了一次同名事件而已。
const listener = () => { console.log('strick'); }; demo.on('loop', () => { demo.on('loop', listener); listener(); }); demo.emit('loop'); // strick demo.emit('loop'); // strick strick
在每次觸發時,列印的數量要比上一次多一個。
3)錯誤處理
在下面這個範例中,由於沒有註冊 error 事件,因此只要一觸發 error 事件就會丟擲錯誤,後面的列印也不會執行。
const EventEmitter = require('events'); const demo = new EventEmitter(); demo.emit('error', new Error('error')); console.log('strick');
將程式碼做下調整,為了防止 Node.js 主執行緒崩潰,應該始終註冊 error 事件,改造後,雖然也會報錯,但是列印仍然能正常執行。
demo.on('error', err => { console.error(err); }); demo.emit('error', new Error('error')); console.log('strick');
參考資料: