從一道面試題簡單談談釋出訂閱和觀察者模式
今天的話題是javascript
中常被提及的「釋出訂閱模式和觀察者模式」,提到這,我不由得想起了一次面試。記得在去年的一次求職面試過程中,面試官問我,“你在專案中是怎麼處理非父子元件之間的通訊的?”。我答道,“有用到vuex
,有的場景也會用EventEmitter2
”。面試官繼續問,“那你能手寫程式碼,實現一個簡單的EventEmitter
嗎?”
手寫EventEmitter
我猶豫了一會兒,想到使用EventEmitter2
時,主要是用emit
發事件,用on
監聽事件,還有off
銷燬事件監聽者,removeAllListeners
銷燬指定事件的所有監聽者,還有once
之類的方法。考慮到時間關係,我想著就先實現發事件,監聽事件,移除監聽者這幾個功能。當時可能有點緊張,不過有驚無險,在面試官給了一點提示後,順利地寫出來了!現在把這部分程式碼也記下來。
class EventEmitter { constructor() { // 維護事件及監聽者 this.listeners = {} } /** * 註冊事件監聽者 * @param {String} type 事件型別 * @param {Function} cb 回撥函式 */ on(type, cb) { if (!this.listeners[type]) { this.listeners[type] = [] } this.listeners[type].push(cb) } /** * 釋出事件 * @param {String} type 事件型別 * @param {...any} args 引數列表,把emit傳遞的引數賦給回撥函式 */ emit(type, ...args) { if (this.listeners[type]) { this.listeners[type].forEach(cb => { cb(...args) }) } } /** * 移除某個事件的一個監聽者 * @param {String} type 事件型別 * @param {Function} cb 回撥函式 */ off(type, cb) { if (this.listeners[type]) { const targetIndex = this.listeners[type].findIndex(item => item === cb) if (targetIndex !== -1) { this.listeners[type].splice(targetIndex, 1) } if (this.listeners[type].length === 0) { delete this.listeners[type] } } } /** * 移除某個事件的所有監聽者 * @param {String} type 事件型別 */ offAll(type) { if (this.listeners[type]) { delete this.listeners[type] } } } // 建立事件管理器例項 const ee = new EventEmitter() // 註冊一個chifan事件監聽者 ee.on('chifan', function() { console.log('吃飯了,我們走!') }) // 釋出事件chifan ee.emit('chifan') // 也可以emit傳遞引數 ee.on('chifan', function(address, food) { console.log(`吃飯了,我們去${address}吃${food}!`) }) ee.emit('chifan', '三食堂', '鐵板飯') // 此時會列印兩條資訊,因為前面註冊了兩個chifan事件的監聽者 // 測試移除事件監聽 const toBeRemovedListener = function() { console.log('我是一個可以被移除的監聽者') } ee.on('testoff', toBeRemovedListener) ee.emit('testoff') ee.off('testoff', toBeRemovedListener) ee.emit('testoff') // 此時事件監聽已經被移除,不會再有console.log打印出來了 // 測試移除chifan的所有事件監聽 ee.offAll('chifan') console.log(ee) // 此時可以看到ee.listeners已經變成空物件了,再emit傳送chifan事件也不會有反應了
有了這個自己寫的簡單版本的EventEmitter
,我們就不用依賴第三方庫啦。對了,vue
也可以幫我們做這樣的事情。
const ee = new Vue();
ee.$on('chifan', function(address, food) { console.log(`吃飯了,我們去${address}吃${food}!`) })
ee.$emit('chifan', '三食堂', '鐵板飯')
所以我們可以單獨new
一個Vue
的例項,作為事件管理器匯出給外部使用。想測試的朋友可以直接開啟vue
官網,在控制檯試試,也可以在自己的vue
釋出訂閱模式
其實仔細看看,EventEmitter
就是一個典型的釋出訂閱模式,實現了事件排程中心。釋出訂閱模式中,包含釋出者,事件排程中心,訂閱者三個角色。我們剛剛實現的EventEmitter
的一個例項ee
就是一個事件排程中心,釋出者和訂閱者是鬆散耦合的,互不關心對方是否存在,他們關注的是事件本身。釋出者借用事件排程中心提供的emit
方法釋出事件,而訂閱者則通過on
進行訂閱。
如果還不是很清楚的話,我們把程式碼換下單詞,是不是變得容易理解一點呢?
class PubSub {
constructor() {
// 維護事件及訂閱行為
this.events = {}
}
/**
* 註冊事件訂閱行為
* @param {String} type 事件型別
* @param {Function} cb 回撥函式
*/
subscribe(type, cb) {
if (!this.events[type]) {
this.events[type] = []
}
this.events[type].push(cb)
}
/**
* 釋出事件
* @param {String} type 事件型別
* @param {...any} args 引數列表
*/
publish(type, ...args) {
if (this.events[type]) {
this.events[type].forEach(cb => {
cb(...args)
})
}
}
/**
* 移除某個事件的一個訂閱行為
* @param {String} type 事件型別
* @param {Function} cb 回撥函式
*/
unsubscribe(type, cb) {
if (this.events[type]) {
const targetIndex = this.events[type].findIndex(item => item === cb)
if (targetIndex !== -1) {
this.events[type].splice(targetIndex, 1)
}
if (this.events[type].length === 0) {
delete this.events[type]
}
}
}
/**
* 移除某個事件的所有訂閱行為
* @param {String} type 事件型別
*/
unsubscribeAll(type) {
if (this.events[type]) {
delete this.events[type]
}
}
}
畫圖分析
最後,我們畫個圖加深下理解:
特點
- 釋出訂閱模式中,對於釋出者
Publisher
和訂閱者Subscriber
沒有特殊的約束,他們好似是匿名活動,藉助事件排程中心提供的介面釋出和訂閱事件,互不瞭解對方是誰。 - 鬆散耦合,靈活度高,常用作事件匯流排
- 易理解,可類比於
DOM
事件中的dispatchEvent
和addEventListener
。
缺點
- 當事件型別越來越多時,難以維護,需要考慮事件命名的規範,也要防範資料流混亂。
觀察者模式
觀察者模式與釋出訂閱模式相比,耦合度更高,通常用來實現一些響應式的效果。在觀察者模式中,只有兩個主體,分別是目標物件Subject
,觀察者Observer
。
- 觀察者需
Observer
要實現update
方法,供目標物件呼叫。update
方法中可以執行自定義的業務程式碼。 - 目標物件
Subject
也通常被叫做被觀察者或主題,它的職能很單一,可以理解為,它只管理一種事件。Subject
需要維護自身的觀察者陣列observerList
,當自身發生變化時,通過呼叫自身的notify
方法,依次通知每一個觀察者執行update
方法。
按照這種定義,我們可以實現一個簡單版本的觀察者模式。
// 觀察者
class Observer {
/**
* 構造器
* @param {Function} cb 回撥函式,收到目標物件通知時執行
*/
constructor(cb){
if (typeof cb === 'function') {
this.cb = cb
} else {
throw new Error('Observer構造器必須傳入函式型別!')
}
}
/**
* 被目標物件通知時執行
*/
update() {
this.cb()
}
}
// 目標物件
class Subject {
constructor() {
// 維護觀察者列表
this.observerList = []
}
/**
* 新增一個觀察者
* @param {Observer} observer Observer例項
*/
addObserver(observer) {
this.observerList.push(observer)
}
/**
* 通知所有的觀察者
*/
notify() {
this.observerList.forEach(observer => {
observer.update()
})
}
}
const observerCallback = function() {
console.log('我被通知了')
}
const observer = new Observer(observerCallback)
const subject = new Subject();
subject.addObserver(observer);
subject.notify();
畫圖分析
最後也整張圖理解下觀察者模式:
特點
- 角色很明確,沒有事件排程中心作為中間者,目標物件
Subject
和觀察者Observer
都要實現約定的成員方法。 - 雙方聯絡更緊密,目標物件的主動性很強,自己收集和維護觀察者,並在狀態變化時主動通知觀察者更新。
缺點
我還沒體會到,這裡不做評價
結語
關於這個話題,網上文章挺多的,觀點上可能也有諸多分歧。重複造輪子,純屬幫助自己加深理解。
本人水平有限,以上僅是個人觀點,如有錯誤之處,還請斧正!如果能幫到您理解發布訂閱模式和觀察者模式,非常榮幸!
如果有興趣看看我這糟糕的程式碼,請點選github,祝大家生活愉快!
首發連結
相關推薦
從一道面試題簡單談談釋出訂閱和觀察者模式
今天的話題是javascript中常被提及的「釋出訂閱模式和觀察者模式」,提到這,我不由得想起了一次面試。記得在去年的一次求職面試過程中,面試官問我,“你在專案中是怎麼處理非父子元件之間的通訊的?”。我答道,“有用到vuex,有的場景也會用EventEmitter2”。面試官繼續問,“那你能手寫程式碼,實現一
從一道面試題來認識java類加載時機與過程【轉】
包含 布局 hello 印象 大致 周期 default () itl 說明:本文的內容是看了《深入理解Java虛擬機:JVM高級特性與最佳實踐》後為加印象和理解,便記錄了重要的內容。 1 開門見山 以前曾經看到過一個java的面試題,當時覺得此題很簡單,可是自己
有關java類、對象初始化的話題,從一道面試題切入
() 深入理解java 補充 [] base sna 字體 都是 spa 最近在整理東西時,剛好碰到以前看的一道有關java類、對象初始化相關題目,覺得答案並不是非常好(記憶點比較差,不是很連貫)。加上剛好復習完類加載全過程的五個階段(加載-驗證-準備-解析-初始化),所以
從一道面試題徹底搞懂hashCode與equals的作用與區別及應當注意的細節
public class HashCodeTest { public static void main(String[] args) { Collection set = new HashSet(); Point p1 = new Point(1, 1); Point p2 = new Poin
從一道面試題來認識java類載入時機與過程
說明:本文的內容是看了《深入理解Java虛擬機器:JVM高階特性與最佳實踐》後為加印象和理解,便記錄了重要的內容。 1 開門見山 以前曾經看到過一個java的面試題,當時覺得此題很簡單,可是自己把程式碼執行起來,可是結果並不是自己想象的那樣。題目如下: class SingleTon {
從一道面試題看ES6箭頭函式
前幾天頭條面試碰到了這樣一道面試題,讓我寫出每行程式碼的執行結果: var f = x => x; f(1); //return 1 var f = x => {x}; f(1); //function(x)={x}; var f = x =
Android多執行緒研究(4)——從一道面試題說起
有一道這樣的面試題:開啟一個子執行緒和主執行緒同時執行,子執行緒輸出10次後接著主執行緒輸出100次,如此反覆50次。先看下面程式碼:package com.maso.test; /** * * @author Administrator * 兩個執行緒,其中是一個
從一道面試題說說方法的引用傳遞和值傳遞
就是說有這麼一道面試題,題目如下: using System; public class Test1 { public static void Main() { int num = 0; Person p = new Pe
從一道面試題談js函式宣告
愛奇藝前端面試題有個題目如下: a(); function a(){ console.log('a'); } b(); var b = function(){ console.log('b'); } 那麼函式的執行結果是? 熟悉函式變數
從一道面試題開始說起 列舉、動態代理的原理
本文已在我的公眾號hongyangAndroid原創釋出。 轉載請標明出處: 本文出自:漲鴻洋的部落格 前段時間在dota群,一哥們出去面試,回顧面試題的時候,說問到了列舉。 作為一名Android選手,談到列舉,那肯定是: An
從一道面試題談linux下fork的執行機制
今天一位朋友去一個不錯的外企面試linux開發職位,面試官出了一個如下的題目:
從一道面試題深入瞭解java虛擬機器記憶體結構
記得剛大學畢業時,為了應付面試,瘋狂的在網上刷JAVA的面試題,很多都靠死記硬背。其中有道面試題,給我的印象非常之深刻,有個大廠的面試官,順著這道題目,一直往下問,問到java虛擬機器的知識,最後把我給問住了。 我當時的表情是這樣的: 後來我有機會面試別人了,也按照他的思路出面試題,很多已經工作了2年的程式設
訂閱發布模式和觀察者模式真的不一樣
參考資料 csharp net 結構 com dash 通知操作 模式 發布-訂閱模式 1、觀察者模式 觀察者模式定義了對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知,並自動更新。觀察者模式屬於行為型模式,行為型模式關註的是對象之
從vue的元件傳值著手淺談觀察者模式
首先,提到觀察者模式,這不禁讓我想到了MVVM,MVVM架構模式感覺用到了觀察者的思想。 我們還是按照慣例,瞭解一下什麼是觀察者模式 觀察者模式又叫釋出訂閱模式,(Publish/Subscribe),完成這個動作首先最少得有兩個不同的物件,或者多個物件,他更像是一種一隊多的依賴關係,也就
Python之美[從菜鳥到高手]--讀"一道面試題看 HashMap 的儲存方式"的聯想
在 HashMap 中存放的一系列鍵值對,其中鍵為某個我們自定義的型別。放入 HashMap 後,我們在外部把某一個 key 的屬性進行更改,然後我們再用這個 key 從 HashMap 裡取出元素,這時候 HashMap 會返回什麼?如何面試者直接答“這要看自定義型別的ha
探索一道面試題的多個解法:C++11 求從 1 到 10 的階乘相加的結果
一、引言 最近,有個朋友出去面試(才畢業不到一年),裡面有一道很簡單的筆試題: 請你編寫程式碼,求 1 到 10 的階乘相加的結果 這是一道非常簡單的題目,我們稍微動一動頭腦,就能拿出下面的程式碼: #include <iostream
一道面試題引發的數據庫行列轉換實踐
聚合函數 列數 index 所有 then 重復 一個 mysq 場景 問題場景 最近有個朋友去面試,問了我一道面試題。題目如下,在形如下面的數據庫表score中,找出每門成績(grade)都大於等於80分的學生姓名。 -------------------------
Java基礎中的一道面試題
override ktr void ati star 打印 str @override ... 這個是我以前的一道面試題: public class MyThread extends Thread { @Override public void run() { tr
關於Java類加載雙親委派機制的思考(附一道面試題)
另類 app 類庫 .com 任務 發現 clas context 表示 預定義類加載器和雙親委派機制 JVM預定義的三種類型類加載器: 啟動(Bootstrap)類加載器:是用本地代碼實現的類裝入器,它負責將 <Java_Runtime_Home>/l
跟濤哥一起學嵌入式 第04集:一道面試題,測出你的C語言功底
inux 臨時 新增 取數據 max 指針 code 個人主頁 ctu 大家好,我是濤哥,歡迎閱讀《跟濤哥一起學嵌入式》第04集,今天聊聊面試題。 嵌入式C語言面試題中,大家經常會看到宏定義的考題。比如:定義一個宏,求兩個數中的最大數。別小看這個考題,雖然簡單,但是它卻陷阱