大家好,我是若川。持續組織了8個月源碼共讀活動,感興趣的可以?點此加我微信ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列。另外:目前建有
江西|湖南|湖北
籍前端群,可加我微信進群。
本文作者有一個設計模式系列:https://pattern.windliang.wang/ 推薦大家收藏保存學習。
代碼也寫了幾年了,設計模式處于看了忘,忘了看的狀態,最近對設計模式有了點感覺,索性就再學習總結下吧。
大部分講設計模式的文章都是使用的 Java
、C++
這樣的以類為基礎的靜態類型語言,作為前端開發者,js
這門基于原型的動態語言,函數成為了一等公民,在實現一些設計模式上稍顯不同,甚至簡單到不像使用了設計模式,有時候也會產生些困惑。
下面按照「場景」-「設計模式定義」- 「代碼實現」-「總」的順序來總結一下,如有不當之處,歡迎交流討論。
場景
假設我們在開發一款外賣網站,進入網站的時候,第一步需要去請求后端接口得到用戶的常用外賣地址。然后再去請求其他接口、渲染頁面。如果什么都不考慮可能會直接這樣寫:
//?getAddress?異步請求
//?頁面里有三個模塊?A,B,C?需要拿到地址后再進行下一步
//?A、B、C?三個模塊都是不同人寫的,提供了不同的方法供我們調用getAddress().then(res?=>?{const?address?=?res.address;A.update(address)B.next(address)C.change(address)
})
此時頁面里多了一個模塊 D
,同樣需要拿到地址后進行下一步操作,我們只好去翻請求地址的代碼把 D
模塊的調用補上。
//?getAddress?異步請求
//?頁面里有三個模塊?A,B,C?需要拿到地址后再進行下一步
//?A、B、C?三個模塊都是不同人寫的,提供了不同的方法供我們調用getAddress().then(res?=>?{const?address?=?res.address;A.update(address)B.next(address)C.change(address)D.init(address)
})
可以看到各個模塊和獲取地址模塊耦合嚴重,A
、B
、C
模塊有變化或者有新增模塊,都需要深入到獲取地址的代碼去修改,一不小心可能就改出問題了。
此時就需要觀察者模式了。
設計模式定義
可以看下 維基百科的介紹:
★The observer pattern is a software design pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.
”
很好理解的一個設計模式,有一個 subject
對象,然后有很多 observers
觀察者對象,當 subject
對象有變化的時候去通知 observer
對象即可。
再看一下 UML
圖和時序圖:

每一個觀察者都實現了 update
方法,并且調用 Subject
對象的 attach
方法訂閱變化。當 Subject
變化時,調用 Observer
的 update
方法去通知觀察者。
先用 java
寫一個簡單的例子:
公眾號文章可以看作是 Subject
,會不定期更新。然后每一個用戶都是一個 Observer
,訂閱公眾號,當更新的時候就可以第一時間收到消息。
import?java.util.ArrayList;interface?Observer?{public?void?update();
}
//?提取?Subject?的公共部分
abstract?class?Subject?{private?ArrayList<Observer>?list?=?new?ArrayList<Observer>();public?void?attach(Observer?observer){list.add(observer);}public?void?detach(Observer?observer){list.remove(observer);}public?void?notifyObserver(){for(Observer?observer?:?list){observer.update();}}
}
//?具體的公眾號,提供寫文章和得到文章
class?WindLiang?extends?Subject?{private?String?post;public?void?writePost(String?p)?{post?=?p;}public?String?getPost()?{return?post;}
}//?小明
class?XiaoMing?implements?Observer?{private?WindLiang?subject;XiaoMing(WindLiang?sub)?{subject?=?sub;}@Overridepublic?void?update(){String?post?=?subject.getPost();System.out.println("我收到了"?+?post?+?"?并且點了個贊");}
}//?小楊
class?XiaoYang?implements?Observer?{private?WindLiang?subject;XiaoYang(WindLiang?sub)?{subject?=?sub;}@Overridepublic?void?update(){String?post?=?subject.getPost();System.out.println("我收到了"?+?post?+?"?并且轉發了");}
}//?小剛
class?XiaoGang?implements?Observer?{private?WindLiang?subject;XiaoGang(WindLiang?sub)?{subject?=?sub;}@Overridepublic?void?update(){String?post?=?subject.getPost();System.out.println("我收到了"?+?post?+?"?并且收藏");}
}public?class?Main?{public?static?void?main(String[]?args)?{WindLiang?windliang?=?new?WindLiang();?//?SubjectXiaoMing?xiaoMing?=?new?XiaoMing(windliang);XiaoYang?xiaoYang?=?new?XiaoYang(windliang);XiaoGang?xiaoGang?=?new?XiaoGang(windliang);//?添加觀察者windliang.attach(xiaoMing);windliang.attach(xiaoYang);windliang.attach(xiaoGang);windliang.writePost("新文章-觀察者模式,balabala");?//?更新文章windliang.notifyObserver();?//?通知觀察者}
}
輸出結果如下:

上邊的實現主要是為了符合最原始的定義,調用 update
的時候沒有傳參。如果觀察者需要的參數是一致的,其實這里也可以直接把更新后的數據傳過去,這樣觀察者就不需要向上邊一樣再去調用 subject.getPost()
手動拿更新后的數據了。
這兩種不同的方式前者叫做拉 (pull)
模式,就是收到 Subject
的通知后,通過內部的 Subject
?對象調用相應的方法去拿到需要的數據。
后者叫做推 (push)
模式,Subject
更新的時候就將數據推給觀察者,觀察者直接使用即可。
下邊用 js
改寫為推模式:
const?WindLiang?=?()?=>?{const?list?=?[];let?post?=?"還沒更新";return?{attach(update)?{list.push(update);},detach(update)?{let?findIndex?=?-1;for?(let?i?=?0;?i?<?list.length;?i++)?{if?(list[i]?===?update)?{findIndex?=?i;break;}}if?(findIndex?!==?-1)?{list.splice(findIndex,?1);}},notifyObserver()?{for?(let?i?=?0;?i?<?list.length;?i++)?{list[i](post);}},writePost(p)?{post?=?p;},};
};const?XiaoMing?=?{update(post){console.log("我收到了"?+?post?+?"?并且點了個贊");}
}const?XiaoYang?=?{update(post){console.log("我收到了"?+?post?+?"?并且轉發了");}
}const?XiaoGang?=?{update(post){console.log("我收到了"?+?post?+?"?并且收藏");}
}windliang?=?WindLiang();windliang.attach(XiaoMing.update)
windliang.attach(XiaoYang.update)
windliang.attach(XiaoGang.update)windliang.writePost("新文章-觀察者模式,balabala")
windliang.notifyObserver()
在 js
中,我們可以直接將 update
方法傳給 Subject
,同時采取推模式,調用 update
的時候直接將數據傳給觀察者,看起來會簡潔很多。
代碼實現
回到開頭的場景,我們可以利用觀察者模式將獲取地址后的一系列后續操作解耦出來。
//?頁面里有三個模塊?A,B,C?需要拿到地址后再進行下一步
//?A、B、C?三個模塊都是不同人寫的,提供了不同的方法供我們調用
const?observers?=?[]
//?注冊觀察者
observers.push(A.update)
observers.push(B.next)
obervers.push(C.change)//?getAddress?異步請求
getAddress().then(res?=>?{const?address?=?res.address;observers.forEach(update?=>?update(address))
})
通過觀察者模式我們將獲取地址后的操作解耦了出來,未來有新增模塊只需要注冊觀察者即可。
當 getAddress
很復雜的時候,通過觀察者模式會使得未來的改動變得清晰,不會影響到 getAddress
的邏輯。
必要的話也可以把 observers
抽離到一個新的文件作為一個新模塊,防止讓一個文件變得過于臃腫。
總
觀察者模式比較好理解,通過抽象出一個 Subject
和多個觀察者,減輕了它們之間的過度耦合。再說簡單點就是利用回調函數,異步完成后調用傳入的回調即可。但上邊寫的觀察者模式還是有一些缺點:
Subject
仍需要自己維護一個觀察者列表,進行push
和update
。如果有其他的模塊也需要使用觀察者模式,還需要模塊本身再維護一個新的觀察者列表,而不能復用之前的代碼。
Subject
需要知道觀察者提供了什么方法以便未來的時候進行回調。
下一篇文章會繼續改進上邊的寫法,觀察者模式的本質思想不變(某個對象變化,然后通知其他觀察者對象進行更新)。
但寫法上會引入一個中間平臺,便于代碼更好的復用,使得 Subject
和觀察者進行更加徹底的解耦,同時給了它一個新的名字「發布訂閱模式」。
更多設計模式推薦閱讀:
前端的設計模式系列-策略模式
前端的設計模式系列-代理模式
前端的設計模式系列-裝飾器模式
本文作者有一個設計模式系列:https://pattern.windliang.wang/ 推薦大家收藏保存學習。
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經堅持寫了8年,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助4000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
掃碼加我微信 ruochuan02、拉你進源碼共讀群
今日話題
目前建有江西|湖南|湖北?籍 前端群,想進群的可以加我微信 ruochuan12?進群。分享、收藏、點贊、在看我的文章就是對我最大的支持~