1、模式介紹
1. 背景介紹
在軟件系統中常常碰到這類需求:當一個對象的狀態發生改變,某些與它相關的對象也要隨之作出相應的變化。這是創建一種「對象與對象之間的依賴關係」,一個對象發生改變時將「自動通知其餘對象」,其餘對象將「相應作出反應」。前端
咱們將發生改變的對象稱爲「觀察目標」,將被通知的對象稱爲「觀察者」,「一個觀察目標能夠對應多個觀察者」,並且這些觀察者之間沒有相互聯繫,以後能夠根據須要增長和刪除觀察者,使得系統更易於擴展,這就是觀察者模式的產生背景。web
2. 概念介紹
觀察者模式(Observer Pattern):定義對象間的一種「一對多依賴關係」,使得每當一個對象狀態發生改變時,其相關依賴對象皆獲得通知並被自動更新。觀察者模式是一種對象行爲型模式。面試
3. 生活場景
在全部瀏覽器事件(鼠標懸停,按鍵等事件)都是觀察者模式的例子。設計模式
另外還有:瀏覽器
如咱們訂閱微信公衆號「前端自習課」(「觀察目標」),當「前端自習課」羣發圖文消息後,全部公衆號粉絲(「觀察者」)都會接收到這篇文章(事件),這篇文章的內容是發佈者自定義的(自定義事件),粉絲閱讀後做出特定操做(如:點贊,收藏,關注等)。性能優化
2、模式特色
1. 模式組成
在觀察者模式中,一般包含如下角色:微信
-
「目標:Subject」 -
「觀察目標:ConcreteSubject」 -
「觀察者:Observer」 -
「具體觀察者:ConcreteObserver」
2. UML 類圖
圖片來源:《TypeScript 設計模式之觀察者模式》app
3. 優勢
-
觀察者模式能夠實現 「表示層和數據邏輯層的分離」,並 「下降觀察目標和觀察者之間耦合度」; -
觀察者模式支持 「簡單廣播通訊」, 「自動通知」全部已經訂閱過的對象; -
觀察者模式 「符合「開閉原則」的要求」; -
觀察目標和觀察者之間的抽象耦合關係可以 「單獨擴展以及重用」。
4. 缺點
-
當一個觀察目標 「有多個直接或間接的觀察者」時,通知全部觀察者的過程將會花費不少時間; -
當觀察目標和觀察者之間存在 「循環依賴」時,觀察目標會觸發它們之間進行循環調用,可能 「致使系統崩潰」。 -
觀察者模式缺乏相應機制,讓觀察者知道所觀察的目標對象是怎麼發生變化的,而僅僅只是知道觀察目標發生了變化。
3、使用場景
在如下狀況下可使用觀察者模式:框架
-
在一個抽象模型中,一個對象的行爲 「依賴於」另外一個對象的狀態。即當 「目標對象」的狀態發生改變時,會直接影響到 「觀察者」的行爲; -
一個對象須要通知其餘對象發生反應,但不知道這些對象是誰; -
須要在系統中建立一個觸發鏈,A對象的行爲將影響B對象,B對象的行爲將影響C對象……,可使用觀察者模式建立一種鏈式觸發機制。
4、實戰示例
1. 簡單示例
-
定義 「觀察目標接口」(Subject)和 「觀察者接口」(Observer)
// ObserverPattern.ts
// 觀察目標接口
interface Subject {
addObserver: (observer: Observer) => void;
deleteObserver: (observer: Observer) => void;
notifyObservers: () => void;
}
// 觀察者接口
interface Observer {
notify: () => void;
}
-
定義 「具體觀察目標類」(ConcreteSubject)
// 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();
});
}
}
-
定義 「具體觀察者類」(ConcreteObserver)
// ObserverPattern.ts
// 具體觀
class ConcreteObserver implements Observer{
constructor(private name: string) {}
notify(): void {
// 能夠處理其餘邏輯
console.log(`${this.name} has been notified.`);
}
}
-
測試代碼
// 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();
完整演示代碼以下:前端性能
// 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();
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
函數,就是咱們的監聽器:
// 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
}
});
}
下面實現訂閱者 Dep
:
// 定義訂閱者類Dep
class Dep {
constructor() {
// 初始化訂閱隊列
this.subs = []
}
// 增長訂閱者
addSub(sub) {
this.subs.push(sub)
}
// 通知訂閱者(是否是全部的代碼都似曾相識?)
notify() {
this.subs.forEach((sub)=>{
sub.update()
})
}
}
如今咱們能夠改寫 defineReactive
中的 setter
方法,在監聽器裏去通知訂閱者了:
function defineReactive(target, key, val) {
const dep = new Dep()
// 監聽當前屬性
observe(val)
Object.defineProperty(target, key, {
set: (value) => {
// 通知全部訂閱者
dep.notify()
}
})
}
5、總結
觀察者模式又稱模型-視圖模式、源-監聽器模式或從屬者模式。是一種「對象行爲型模式」。其定義了一種「對象間的一對多依賴關係」,當觀察目標發生狀態變化,會通知全部觀察者對象,使它們自動更新。
在實際業務中,若是一個對象的行爲「依賴於」另外一個對象的狀態。或者說當「目標對象」的狀態發生改變時,會直接影響到「觀察者」的行爲,儘可能考慮到使用觀察者模式來實現。
6、拓展
觀察者模式和發佈-訂閱模式二者很像,但其實區別比較大。例如:
-
耦合度差別:觀察者模式的耦合度就比發佈-訂閱模式要高; -
關注點不一樣:觀察者模式須要知道彼此的存在,而發佈-訂閱模式則是經過調度中心來聯繫發佈/訂閱者。
下一篇文章見。
參考文章
1.《3. 觀察者模式》
2.《TypeScript 設計模式之觀察者模式》
3.《JavaScript 設計模式核⼼原理與應⽤實踐》
本期獎品:《Web前端性能優化》*1
天天參與「前端自習課」早讀打卡活動,一週打滿 5 次,便可參與送書抽獎活動。
打卡方式:當天推文評論區評論任意內容表示參與(讀書感悟,心情等)。
活動第二週週一統計並公衆號推文參與抽獎的名單,將知足打卡次數的小夥伴拉羣參與抽獎。我在羣裏發紅包,手氣最佳者爲中獎(相同時繼續發紅包)。
打卡時間:08.17 - 08.23;
抽獎時間:08.24 20:00。
本文分享自微信公衆號 - 前端自習課(FE-study)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。