速通 JavaScript 代理模式和發布訂閱模式
1. 前言
JavaScript 是一門動態語言,在實現設計模式的時候,往往會比 Java 等靜態語言更簡便,本文將介紹在 JavaScript 中如何實現代理模式和發布訂閱模式。
2. 代理模式
2.1. 定義
在介紹定義時還是以類圖為主,雖然 JavaScript 實現設計模式時可能不會使用到類,但是類圖提供了一種通用的設計模式實現思想。
代理模式定義:為其他對象提供一種代理以控制對這個對象的訪問。
其類圖如下:
圖片
類圖中的三個角色:
- Subject 抽象主題角色:定義了具體主題和代理主題的共同接口,這樣在任何使用具體主題的地方都可以使用代理主題。
- RealSubject 具體主題角色:邏輯的具體執行者。
- Proxy 代理主題角色:實現了抽象主題接口,并持有對具體主題的引用。
2.2. 實現
在 JavaScript 中,你可以使用 Proxy 輕松實現代理模式,比如可以通過代理模式實現一個只接收 number 類型值的數組。
const arr = []
const numArr = new Proxy(arr, {
set(target, key, value, proxy) {
if (typeof value !== 'number') {
throw Error("屬性只能是 number 類型");
}
return Reflect.set(target, key, value, proxy);
}
})
numArr.push(0)
numArr.push('1') // Uncaught Error: 屬性只能是 number 類型
console.log(numArr) // Proxy(Array) {0: 0}
利用 Proxy,你還可以實現響應式編程。
const data = { userName: '' }
const render = (info) => {
console.log(info)
// 根據數據渲染界面
}
const proxyData = new Proxy(data, {
set(target, key, value, receiver) {
// 設置值
Reflect.set(target, key, value, receiver)
// 重新觸發渲染
render(target)
}
})
data.userName = 'xiaoming'// 控制臺輸出 { userName: 'xiaoming' }
當然你也可以利用 Proxy 來實現日志功能,用于跟蹤函數調用情況。
function add(a, b) {
return a + b;
}
// 日志記錄函數
function log(message) {
console.log(message);
}
// 創建代理對象
const proxy = newProxy(add, {
// 攔截函數調用
apply(target, thisArg, args) {
const result = Reflect.apply(target, thisArg, args);
log(`函數 ${target.name} 被調用,參數: [${args.join(', ')}],返回值: ${result}`);
return result;
}
});
const sum1 = proxy(1, 2); // 輸出: 函數 add 被調用,參數: [1, 2],返回值: 3
const sum2 = proxy(3, 4); // 輸出: 函數 add 被調用,參數: [3, 4],返回值: 7
2.3. 小結
最后提一下代理模式和裝飾模式的異同點,兩者的共同點是代理類或裝飾類和原本類都具有相同的接口,不同點則是代理模式著重對代理過程的控制,而裝飾模式則是對類的功能進行加強或減弱。
3. 發布訂閱模式
3.1. 定義
發布訂閱模式的定義:定義對象間一種一對多的依賴關系,使得每當一個對象改變狀態,則所有依賴于它的對象都會得到通知并被自動更新。
其類圖如下:
圖片
發布訂閱模式經常會和觀察者模式做對比,兩個設計模式廣義上設計理念是一致的,在實現上有些差別,本文更注重實際應用,故不展開此內容,借用一張圖來說明。
圖片
3.2. 實現
接著來完成發布訂閱模式的簡單實現,主要是實現 subscribe 和 publish 方法。
const event = {
listeners: [], // 所有訂閱者集合
// 訂閱函數
subscribe: function(fn) {
this.listeners.push(fn)
},
// 發布函數
publish: function() {
for(let i = 0; i < this.listeners.length; i++) {
this.listeners[i]()
}
},
// 移除訂閱函數
unsubcribe: function(fn) {
const fns = this.listeners;
// 倒序訪問方便使用 splice 移除訂閱函數
for (let l = fns.length - 1; l >=0; l--) {
const _fn = fns[l];
if (_fn === fn){
fns.splice(l, 1);
}
}
}
}
const fn1 = () => { console.log('trigger1') }
const fn2 = () => { console.log('trigger2') }
event.subscribe(fn1)
event.subscribe(fn2)
event.publish() // 控制臺打印 trigger1, trigger2
event.unsubcribe(fn1)
event.publish() // 控制臺打印 trigger2
到此我們實現了一個簡單版本的發布訂閱。
接下來我們基于發布訂閱模式,在 React 中實現一個類似 Zustand 的狀態管理功能。
首先我們需要了解一個 React 官方 Hook useSyncExternalStore,這個 Hook 可以讓你訂閱一個外部數據源,當其中數據發生變化時,React 會觸發重新渲染。
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
可以看到該 Hook 參數有三個,這里我們主要關注前兩個,第一個參數即訂閱函數,第二個參數為獲取數據源的函數,第三個和服務端渲染相關。
接下來我們要結合發布訂閱模式和 useSyncExternalStore 實現一個簡單版本的 Zustand。
const createImpl = (createState) => {
// 相比發布訂閱模式,多了個狀態值
let state
let initialState
const listeners = newSet()
// 類似發布訂閱模式中的 publish 方法,最終會觸發訂閱者
const setState = (nextState) => {
// 對比狀態值是否有變化
if (!Object.is(nextState, state)) {
const previousState = state
state = Object.assign({}, state, nextState)
// 觸發訂閱函數
listeners.forEach((listener) => listener(state, previousState))
}
}
const getState = () => state
const getInitialState = () => initialState
const subscribe = (listener) => {
listeners.add(listener)
// 返回一個取消訂閱的方法
return() => {
listeners.delete(listener)
}
}
// 清空訂閱
const destory = () => listeners.clear()
const api = {
setState,
getState,
getInitialState,
subscribe,
destory
}
// 調用 createState 方法返回初始狀態值,createState 參數為 set、get 和 api 對象
initialState = (state = createState(setState, getState, api))
return api
}
const create = (createState) => {
const api = createImpl(createState)
// 傳入訂閱方法和獲取數據方法到 useSyncExternalStore
const useStore = () => useSyncExternalStore(api.subscribe, api.getState)
// 把 api 合并到 Hook 上
Object.assign(useStore, api)
return useStore
}
exportdefault create
先來看下 createImpl 函數,相比于我們實現的簡單版發布訂閱模式,createImpl 內部多維護了一個狀態值,在調用發布方法(setState)時,會更新狀態值,并觸發訂閱函數,訂閱函數入參為新舊狀態值。
最后我們看下如何使用自己的狀態管理功能。
// create 方法接收一個函數參數,內部會調用函數初始化狀態值,最終返回一個 Hook
const useStore = create((set) => ({
num: 1,
// 通過 set 方法更新狀態值,更新后觸發所有訂閱函數的調用
random: () =>set({ num: Math.round(Math.random() * 1000) }),
}))
function Counter() {
// 調用 useStore,useStore 會調用 React useSyncExternalStore
const { num, random } = useStore();
return (
<div>
<p>{`Number: ${num}`}</p>
<button onClick={random}>Random</button>
</div>
)
}
create 方法接收一個函數參數,用于初始化狀態,最終 create 會返回一個 Hook。在狀態值中, random 方法會調用發布方法(setState)觸發更新,因為 useSyncExternalStore 會使用第一個參數完成訂閱動作,所以此時它能接收到數據更新,隨后便返回最新的狀態值,并觸發重新渲染。
在線代碼示例:https://stackblitz.com/edit/react-9nvjhwhx?file=demo.tsx
至此我們實現了一個簡單版本 Zustand。
3.3. 小結
發布訂閱模式的優點非常明顯,一為時間上的解耦,二為對象之間的解耦,但如果過度使用的話,對象和對象之間的必要聯系也將被深埋在背后,會導致程序難以跟蹤維護和理解。
3. 總結
設計模式大體思想是要把系統中不變和變化的部分分開,封裝不變的部分,根據業務靈活替換變化的部分,這樣就可以保證系統的健壯性和可拓展性。同時在實現設計模式的同時,你通常也會很好的遵守了設計模式原則,如單一職責、依賴倒置、開閉原則、迪米特原則等。