Events 是 Node.js 中最重要的核心模塊之一,很多模塊都是依賴其創建的,例如[上一節分析的流](https://www.cnblogs.com/strick/p/16225418.html),文件、網絡等模塊。
  比較知名的 Express、KOA 等框架在其內部也使用了 Events 模塊。
  Events 模塊提供了[EventEmitter](https://nodejs.org/dist/latest-v18.x/docs/api/events.html)類,EventEmitter 也叫事件觸發器,是一種觀察者模式的實現。
  觀察者模式是軟件設計模式的一種,在此模式中,一個目標對象(即被觀察者對象)管理所有依賴于它的觀察者對象。
  當其自身狀態發生變化時,將以廣播的方式主動發送通知(在通知中可攜帶一些數據),這樣就能在兩者之間建立觸發機制,達到解耦地目的。
  與瀏覽器中的事件處理器不同,在 Node.js 中沒有捕獲、冒泡、preventDefault() 等概念或方法。
  本系列所有的示例源碼都已上傳至Github,[點擊此處](https://github.com/pwstrick/node)獲取。
## 一、方法原理
  在下面的示例中,加載 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](https://github.com/nodejs/node/blob/master/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');
~~~
參考資料:
[Node.js技術棧之事件觸發器](https://www.nodejs.red/#/nodejs/events)?[異步迭代器](https://mp.weixin.qq.com/s/PDCZ5FreFJDJDqpvOe3xKQ)?
[餓了么事件異步面試題](https://github.com/ElemeFE/node-interview/tree/master/sections/zh-cn#%E4%BA%8B%E4%BB%B6%E5%BC%82%E6%AD%A5)
[深入理解Node.js之Event](https://yjhjstz.gitbooks.io/deep-into-node/content/chapter7/chapter7-1.html)
[Node.js事件模塊](http://nodejs.cn/learn/the-nodejs-events-module)?[events事件模塊](https://nodejs.org/dist/latest-v18.x/docs/api/events.html)
[EventEmitter 源碼分析與簡易實現](https://segmentfault.com/a/1190000016654243)
[源碼分析:EventEmitter](https://juejin.cn/post/6969843023190425636)
[詳解Object.create(null)](https://juejin.cn/post/6844903589815517192)
*****
> 原文出處:
[博客園-Node.js精進](https://www.cnblogs.com/strick/category/2154090.html)
[知乎專欄-前端性能精進](https://www.zhihu.com/column/c_1611672656142725120)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎瀏覽。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020