TypeScript 設計模式之觀察者模式

一、模式介紹

1. 背景介紹

在軟件系統中經常碰到這類需求:當一個對象的狀態發生改變,某些與它相關的對象也要隨之做出相應的變化。這是建立一種「對象與對象之間的依賴關係」,一個對象發生改變時將「自動通知其他對象」,其他對象將「相應做出反應」

我們將發生改變的對象稱為「觀察目標」,將被通知的對象稱為「觀察者」「一個觀察目標可以對應多個觀察者」,而且這些觀察者之間沒有相互聯繫,之後可以根據需要增加和刪除觀察者,使得系統更易於擴展,這就是觀察者模式的產生背景。

2. 概念介紹

觀察者模式(Observer Pattern):定義對象間的一種「一對多依賴關係」

,使得每當一個對象狀態發生改變時,其相關依賴對象皆得到通知並被自動更新。

觀察者模式又稱「發佈-訂閱(Publish/Subscribe)模式」、模型-視圖(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。觀察者模式是一種對象行為型模式。

3. 生活場景

在所有瀏覽器事件(鼠標懸停,按鍵等事件)都是觀察者模式的例子。

另外還有:

如我們訂閱微信公眾號“前端自習課”(「觀察目標」),當“前端自習課”群發圖文消息後,所有公眾號粉絲(「觀察者」)都會接收到這篇文章(事件),這篇文章的內容是發佈者自定義的(自定義事件),粉絲閱讀後作出特定操作(如:點贊,收藏,關注等)。

觀察者模式.png

二、模式特點

1. 模式組成

在觀察者模式中,通常包含以下角色:

「目標:Subject」「觀察目標:ConcreteSubject」「觀察者:Observer」「具體觀察者:ConcreteObserver」

2. UML 類圖

UML 類圖

圖片來源:《TypeScript 設計模式之觀察者模式》

3. 優點

觀察者模式可以實現「表示層和數據邏輯層的分離」,並「降低觀察目標和觀察者之間耦合度」;觀察者模式支持「簡單廣播通信」「自動通知」
所有已經訂閱過的對象;觀察者模式「符合“開閉原則”的要求」;觀察目標和觀察者之間的抽象耦合關係能夠「單獨擴展以及重用」

4. 缺點

當一個觀察目標「有多個直接或間接的觀察者」時,通知所有觀察者的過程將會花費很多時間;當觀察目標和觀察者之間存在「循環依賴」時,觀察目標會觸發它們之間進行循環調用,可能「導致系統崩潰」。觀察者模式缺少相應機制,讓觀察者知道所觀察的目標對象是怎麼發生變化的,而僅僅只是知道觀察目標發生了變化。

三、使用場景

在以下情況下可以使用觀察者模式:

在一個抽象模型中,一個對象的行為「依賴於」另一個對象的狀態。即當「目標對象」的狀態發生改變時,會直接影響到
「觀察者」的行為;一個對象需要通知其他對象發生反應,但不知道這些對象是誰。需要在系統中創建一個觸發鏈,A對象的行為將影響B對象,B對象的行為將影響C對象……,可以使用觀察者模式創建一種鏈式觸發機制。

四、實戰示例

1. 簡單示例

定義「觀察目標接口」(Subject)和「觀察者接口」(Observer)

<code>// ObserverPattern.ts // 觀察目標接口 interface Subject {   addObserver: (observer: Observer) => void;   deleteObserver: (observer: Observer) => void;   notifyObservers: () => void; } // 觀察者接口 interface Observer {   notify: () => void; }/<code>定義「具體觀察目標類」(ConcreteSubject)

<code>// ObserverPattern.ts // 具體觀察目標類 class ConcreteSubject implements Subject{    private observers: Observer[] = [];     // 添加觀察者   public addObserver(observer: Observer): void {     console.log(observer, " is pushed~~");     this.observers.push(observer);   }   // 移除觀察者   public deleteObserver(observer: Observer): void {     console.log(observer, " have deleted~~");     const idx: number = this.observers.indexOf(observer);     ~idx && this.observers.splice(idx, 1);   }   // 通知觀察者   public notifyObservers(): void {     console.log("notify all the observers ", this.observers);     this.observers.forEach(observer => {        // 調用 notify 方法時可以攜帶指定參數       observer.notify();     });   } }/<code>定義「具體觀察者類」(ConcreteObserver)

<code>// ObserverPattern.ts // 具體觀 class ConcreteObserver implements Observer{   constructor(private name: string) {}   notify(): void {     // 可以處理其他邏輯     console.log(`${this.name} has been notified.`);   } }/<code>測試代碼

<code>// ObserverPattern.ts function useObserver(): void {   const subject: Subject = new ConcreteSubject();   const Leo   = new ConcreteObserver("Leo");   const Robin = new ConcreteObserver("Robin");   const Pual  = new ConcreteObserver("Pual");   const Lisa  = new ConcreteObserver("Lisa");   subject.addObserver(Leo);   subject.addObserver(Robin);   subject.addObserver(Pual);   subject.addObserver(Lisa);   subject.notifyObservers();      subject.deleteObserver(Pual);   subject.deleteObserver(Lisa);   subject.notifyObservers(); } useObserver();/<code>

完整演示代碼如下:

<code>// ObserverPattern.ts interface Subject {   addObserver: (observer: Observer) => void;   deleteObserver: (observer: Observer) => void;   notifyObservers: () => void; } interface Observer {   notify: () => void; } class ConcreteSubject implements Subject{    private observers: Observer[] = [];   public addObserver(observer: Observer): void {     console.log(observer, " is pushed~~");     this.observers.push(observer);   }   public deleteObserver(observer: Observer): void {     console.log(observer, " have deleted~~");     const idx: number = this.observers.indexOf(observer);     ~idx && this.observers.splice(idx, 1);   }   public notifyObservers(): void {     console.log("notify all the observers ", this.observers);     this.observers.forEach(observer => {        // 調用 notify 方法時可以攜帶指定參數       observer.notify();     });   } } class ConcreteObserver implements Observer{   constructor(private name: string) {}   notify(): void {     // 可以處理其他邏輯     console.log(`${this.name} has been notified.`);   } } function useObserver(): void {   const subject: Subject = new ConcreteSubject();   const Leo   = new ConcreteObserver("Leo");   const Robin = new ConcreteObserver("Robin");   const Pual  = new ConcreteObserver("Pual");   const Lisa  = new ConcreteObserver("Lisa");   subject.addObserver(Leo);   subject.addObserver(Robin);   subject.addObserver(Pual);   subject.addObserver(Lisa);   subject.notifyObservers();      subject.deleteObserver(Pual);   subject.deleteObserver(Lisa);   subject.notifyObservers(); } useObserver();/<code>

2. Vue.js 數據雙向綁定實現原理

在 Vue.js 中,當我們修改數據狀時,視圖隨之更新,這就是 Vue.js 的雙向數據綁定(也稱響應式原理),這是 Vue.js 中最獨特的特性之一。 如果你對 Vue.js 的雙向數據綁定還不清楚,建議先閱讀官方文檔《深入響應式原理》章節。

2.1 原理介紹

在官網中提供這麼一張流程圖,介紹了 Vue.js 響應式系統的整個流程:

圖片來自:Vue.js 官網《深入響應式原理》

在 Vue.js 中,每個組件實例都對應一個 watcher 實例,它會在組件渲染的過程中把“接觸”(“Touch” 過程)過的數據 property 記錄為依賴(Collect as Dependency 過程)。之後當依賴項的 setter 觸發時,會通知 watcher(Notify 過程),從而使它關聯的組件重新渲染(Trigger re-render 過程)——這是一個典型的觀察者模式。

這道面試題考察面試者對 Vue.js 底層原理的理解、對觀察者模式的實現能力以及一系列重要的JS知識點,具有較強的綜合性和代表性。

2.2 組成部分

在 Vue.js 數據雙向綁定的實現邏輯中,包含三個關鍵角色:

observer(監聽器):這裡的 observer 不僅是訂閱者(「需要監聽數據變化」),同時還是發佈者(「對監聽的數據進行轉發」)。watcher(訂閱者):watcher對象是**真正的訂閱者, **observer 把數據轉發給 watcher 對象。watcher 接收到新的數據後,執行視圖更新。compile(編譯器):MVVM 框架特有的角色,負責對每個節點元素指令進行掃描和解析,處理指令的數據初始化、訂閱者的創建等操作。

這三者的配合過程如圖所示:

圖片來自:掘金小冊《JavaScript 設計模式核⼼原理與應⽤實踐》

2.3 實現核心代碼 observer

首先我們需要實現一個方法,這個方法會對需要監聽的數據對象進行遍歷、給它的屬性加上定製的 getter 和 setter 函數。這樣但凡這個對象的某個屬性發生了改變,就會觸發 setter 函數,進而通知到訂閱者。這個 setter 函數,就是我們的監聽器:

<code>// observe方法遍歷幷包裝對象屬性 function observe(target) {     // 若target是一個對象,則遍歷它     if(target && typeof target === 'object') {         Object.keys(target).forEach((key)=> {             // defineReactive方法會給目標屬性裝上“監聽器”             defineReactive(target, key, target[key])         })     } } // 定義defineReactive方法 function defineReactive(target, key, val) {     // 屬性值也可能是object類型,這種情況下需要調用observe進行遞歸遍歷     observe(val)     // 為當前屬性安裝監聽器     Object.defineProperty(target, key, {          // 可枚舉         enumerable: true,         // 不可配置         configurable: false,          get: function () {             return val;         },         // 監聽器函數         set: function (value) {             console.log(`${target}屬性的${key}屬性從${val}值變成了了${value}`)             val = value         }     }); }/<code>

下面實現訂閱者 Dep:

<code>// 定義訂閱者類Dep class Dep {     constructor() {         // 初始化訂閱隊列         this.subs = []     }          // 增加訂閱者     addSub(sub) {         this.subs.push(sub)     }          // 通知訂閱者(是不是所有的代碼都似曾相識?)     notify() {         this.subs.forEach((sub)=>{             sub.update()         })     } }/<code>

現在我們可以改寫 defineReactive 中的 setter 方法,在監聽器裡去通知訂閱者了:

<code>function defineReactive(target, key, val) {     const dep = new Dep()     // 監聽當前屬性     observe(val)     Object.defineProperty(target, key, {         set: (value) => {             // 通知所有訂閱者             dep.notify()         }     }) }/<code>

五、總結

觀察者模式又稱發佈-訂閱模式、模型-視圖模式、源-監聽器模式或從屬者模式。是一種「對象行為型模式」。其定義了一種「對象間的一對多依賴關係」,當觀察目標發生狀態變化,會通知所有觀察者對象,使它們自動更新。

在實際業務中,如果一個對象的行為「依賴於」另一個對象的狀態。或者說當「目標對象」的狀態發生改變時,會直接影響到「觀察者」的行為,儘量考慮到使用觀察者模式來實現。

六、拓展

觀察者模式和發佈-訂閱模式兩者很像,但其實區別比較大。例如:

耦合度差異:觀察者模式的耦合度就比發佈-訂閱模式要高;關注點不同:觀察者模式需要知道彼此的存在,而發佈-訂閱模式則是通過調度中心來聯繫發佈/訂閱者。

下一篇文章見。

參考文章

1.《3. 觀察者模式》

2.《TypeScript 設計模式之觀察者模式》

3.《JavaScript 設計模式核⼼原理與應⽤實踐》