# 如何實現一個Event
點擊關注本[公眾號](http://www.hmoore.net/book/dsh225/javascript_vue_css/edit#_118)獲取文檔最新更新,并可以領取配套于本指南的《**前端面試手冊**》以及**最標準的簡歷模板**.
[TOC]
## 前言
本文標題的題目是由其他問題延伸而來,面試中面試官的常用套路,揪住一個問題一直深挖,在產生這個問題之前一定是這個問題.
> React/Vue不同組件之間是怎么通信的?
**Vue**
1. 父子組件用Props通信
2. 非父子組件用Event Bus通信
3. 如果項目夠復雜,可能需要Vuex等全局狀態管理庫通信
4. `$dispatch`(已經廢除)和`$broadcast`(已經廢除)
**React**
1. 父子組件,父->子直接用Props,子->父用callback回調
2. 非父子組件,用發布訂閱模式的Event模塊
3. 項目復雜的話用Redux、Mobx等全局狀態管理管庫
4. 用新的[Context Api](https://juejin.im/post/5a7b41605188257a6310fbec)
我們大體上都會有以上回答,接下來很可能會問到如何實現`Event(Bus)`,因為這個東西太重要了,幾乎所有的模塊通信都是基于類似的模式,包括安卓開發中的`Event Bus`,Node.js中的`Event`模塊(Node中幾乎所有的模塊都依賴于Event,包括不限于`http、stream、buffer、fs`等).
我們仿照Node中[Event API](http://nodejs.cn/api/events.html)實現一個簡單的Event庫,他是**發布訂閱模式**的典型應用.
> **提前聲明:**我們沒有對傳入的參數進行及時判斷而規避錯誤,僅僅對核心方法進行了實現.
## 基本構造
### 初始化class
我們利用ES6的`class`關鍵字對`Event`進行初始化,包括`Event`的事件清單和監聽者上限.
我們選擇了`Map`作為儲存事件的結構,因為作為鍵值對的儲存方式`Map`比一般對象更加適合,我們操作起來也更加簡潔,可以先看一下Map的[基本用法與特點](http://es6.ruanyifeng.com/#docs/set-map#Map).
~~~
class EventEmeitter {
constructor() {
this._events = this._events || new Map(); // 儲存事件/回調鍵值對
this._maxListeners = this._maxListeners || 10; // 設立監聽上限
}
}
~~~
### 監聽與觸發
觸發監聽函數我們可以用`apply`與`call`兩種方法,在少數參數時`call`的性能更好,多個參數時`apply`性能更好,當年Node的Event模塊就在三個參數以下用`call`否則用`apply`.
當然當Node全面擁抱ES6+之后,相應的`call/apply`操作用`Reflect`新關鍵字重寫了,但是我們不想寫的那么復雜,就做了一個簡化版.
~~~
// 觸發名為type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
// 從儲存事件鍵值對的this._events中獲取對應事件回調函數
handler = this._events.get(type);
if (args.length > 0) {
handler.apply(this, args);
} else {
handler.call(this);
}
return true;
};
// 監聽名為type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
// 將type事件以及對應的fn函數放入this._events中儲存
if (!this._events.get(type)) {
this._events.set(type, fn);
}
};
~~~
我們實現了觸發事件的`emit`方法和監聽事件的`addListener`方法,至此我們就可以進行簡單的實踐了.
~~~
// 實例化
const emitter = new EventEmeitter();
// 監聽一個名為arson的事件對應一個回調函數
emitter.addListener('arson', man => {
console.log(`expel ${man}`);
});
// 我們觸發arson事件,發現回調成功執行
emitter.emit('arson', 'low-end'); // expel low-end
~~~
似乎不錯,我們實現了基本的觸發/監聽,但是如果有多個監聽者呢?
~~~
// 重復監聽同一個事件名
emitter.addListener('arson', man => {
console.log(`expel ${man}`);
});
emitter.addListener('arson', man => {
console.log(`save ${man}`);
});
emitter.emit('arson', 'low-end'); // expel low-end
~~~
是的,只會觸發第一個,因此我們需要進行改造.
## 升級改造
### 監聽/觸發器升級
我們的`addListener`實現方法還不夠健全,在綁定第一個監聽者之后,我們就無法對后續監聽者進行綁定了,因此我們需要將后續監聽者與第一個監聽者函數放到一個數組里.
~~~
// 觸發名為type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
handler = this._events.get(type);
if (Array.isArray(handler)) {
// 如果是一個數組說明有多個監聽者,需要依次此觸發里面的函數
for (let i = 0; i < handler.length; i++) {
if (args.length > 0) {
handler[i].apply(this, args);
} else {
handler[i].call(this);
}
}
} else { // 單個函數的情況我們直接觸發即可
if (args.length > 0) {
handler.apply(this, args);
} else {
handler.call(this);
}
}
return true;
};
// 監聽名為type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
const handler = this._events.get(type); // 獲取對應事件名稱的函數清單
if (!handler) {
this._events.set(type, fn);
} else if (handler && typeof handler === 'function') {
// 如果handler是函數說明只有一個監聽者
this._events.set(type, [handler, fn]); // 多個監聽者我們需要用數組儲存
} else {
handler.push(fn); // 已經有多個監聽者,那么直接往數組里push函數即可
}
};
~~~
是的,從此以后可以愉快的觸發多個監聽者的函數了.
~~~
// 監聽同一個事件名
emitter.addListener('arson', man => {
console.log(`expel ${man}`);
});
emitter.addListener('arson', man => {
console.log(`save ${man}`);
});
emitter.addListener('arson', man => {
console.log(`kill ${man}`);
});
// 觸發事件
emitter.emit('arson', 'low-end');
//expel low-end
//save low-end
//kill low-end
~~~
### 移除監聽
我們會用`removeListener`函數移除監聽函數,但是匿名函數是無法移除的.
~~~
EventEmeitter.prototype.removeListener = function(type, fn) {
const handler = this._events.get(type); // 獲取對應事件名稱的函數清單
// 如果是函數,說明只被監聽了一次
if (handler && typeof handler === 'function') {
this._events.delete(type, fn);
} else {
let postion;
// 如果handler是數組,說明被監聽多次要找到對應的函數
for (let i = 0; i < handler.length; i++) {
if (handler[i] === fn) {
postion = i;
} else {
postion = -1;
}
}
// 如果找到匹配的函數,從數組中清除
if (postion !== -1) {
// 找到數組對應的位置,直接清除此回調
handler.splice(postion, 1);
// 如果清除后只有一個函數,那么取消數組,以函數形式保存
if (handler.length === 1) {
this._events.set(type, handler[0]);
}
} else {
return this;
}
}
};
~~~
### 發現問題
我們已經基本完成了`Event`最重要的幾個方法,也完成了升級改造,可以說一個`Event`的骨架是被我們開發出來了,但是它仍然有不足和需要補充的地方.
> 1. 魯棒性不足: 我們沒有對參數進行充分的判斷,沒有完善的報錯機制.
> 2. 模擬不夠充分: 除了`removeAllListeners`這些方法沒有實現以外,例如監聽時間后會觸發`newListener`事件,我們也沒有實現,另外最開始的監聽者上限我們也沒有利用到.
當然,這在面試中現場寫一個Event已經是很夠意思了,主要是體現出來對**發布-訂閱**模式的理解,以及針對多個監聽狀況下的處理,不可能現場擼幾百行寫一個完整Event.
索性[Event](https://github.com/Gozala/events/blob/master/events.js)庫幫我們實現了完整的特性,整個代碼量有300多行,很適合閱讀,你可以花十分鐘的時間通讀一下,見識一下完整的Event實現.
* * *
## 公眾號
想要實時關注筆者最新的文章和最新的文檔更新請關注公眾號**程序員面試官**,后續的文章會優先在公眾號更新.
**簡歷模板**:關注公眾號回復「模板」獲取
**《前端面試手冊》**:配套于本指南的突擊手冊,關注公眾號回復「fed」獲取

- 前言
- 指南使用手冊
- 為什么會有這個項目
- 面試技巧
- 面試官到底想看什么樣的簡歷?
- 面試回答問題的技巧
- 如何通過HR面
- 推薦
- 書籍/課程推薦
- 前端基礎
- HTML基礎
- CSS基礎
- JavaScript基礎
- 瀏覽器與新技術
- DOM
- 前端基礎筆試
- HTTP筆試部分
- JavaScript筆試部分
- 前端原理詳解
- JavaScript的『預解釋』與『變量提升』
- Event Loop詳解
- 實現不可變數據
- JavaScript內存管理
- 實現深克隆
- 如何實現一個Event
- JavaScript的運行機制
- 計算機基礎
- HTTP協議
- TCP面試題
- 進程與線程
- 數據結構與算法
- 算法面試題
- 字符串類面試題
- 前端框架
- 關于前端框架的面試須知
- Vue面試題
- React面試題
- 框架原理詳解
- 虛擬DOM原理
- Proxy比defineproperty優劣對比?
- setState到底是異步的還是同步的?
- 前端路由的實現
- redux原理全解
- React Fiber 架構解析
- React組件復用指南
- React-hooks 抽象組件
- 框架實戰技巧
- 如何搭建一個組件庫的開發環境
- 組件設計原則
- 實現輪播圖組件
- 性能優化
- 前端性能優化-加載篇
- 前端性能優化-執行篇
- 工程化
- webpack面試題
- 前端工程化
- Vite
- 安全
- 前端安全面試題
- npm
- 工程化原理
- 如何寫一個babel
- Webpack HMR 原理解析
- webpack插件編寫
- webpack 插件化設計
- Webpack 模塊機制
- webpack loader實現
- 如何開發Babel插件
- git
- 比較
- 查看遠程倉庫地址
- git flow
- 比較分支的不同并保存壓縮文件
- Tag
- 回退
- 前端項目經驗
- 確定用戶是否在當前頁面
- 前端下載文件
- 只能在微信中訪問
- 打開新頁面-被瀏覽器攔截
- textarea高度隨內容變化 vue版
- 去掉ios原始播放大按鈕
- nginx在MAC上的安裝、啟動、重啟和關閉
- 解析latex格式的數學公式
- 正則-格式化a鏈接
- 封裝的JQ插件庫
- 打包問題總結
- NPM UI插件
- 帶你入門前端工程
- webWorker+indexedDB性能優化
- 多個相鄰元素切換效果出現邊框重疊問題的解決方法
- 監聽前端storage變化