一、觀察者模式
1. 什麼是觀察者模式
當對象之間存在一對多的依賴關係時,其中一個對象的狀態發生改變,所有依賴它的對象都會收到通知,這就是觀察者模式。
2. 實際場景
1. DOM
事件
開發過程中,最常見的觀察者模式場景就是 DOM
事件函數,先看看代碼:
document.body.addEventListener('click', () => {
alert(2)
}, false)
當 body
節點被點擊時,觸發 alert(2)
,從觀察者模式來解釋,就是我們訂閱了 bdoy
節點的點擊事件,當點擊事件觸發,我們就會收到通知。
2. 網站登錄
網站登錄功能,想必大多數做過平臺需求的同學都實現過,當網站中的不同模塊,如 Header 模塊、Nav 模塊、正文模塊,都依賴登錄後獲取的用戶數據時,該怎麼去實現呢?
2.1 普通做法
先看代碼:
login.succ((data => {
header.setAvatar(data.avatar) // 設置頭像
nav.setAvatar(data.avatar) // 設置導航區的頭像
list.refresh() // 刷新列表
})
這樣的代碼是不是特別熟悉?把依賴的方法,放在回調函數中。上述就是在登錄成功的回調函數中,添加了各模塊的方法。這麼做導致各個模塊和登錄模塊高度耦合,當新增了一個地址欄模塊時,不得不再次修改登錄模塊的回調函數,違反了開放-封閉原則。
2.2 觀察者模式
用觀察者模式,優化上述需求。
登錄模塊是一個訂閱對象,Header 模塊、Nav 模塊、正文模塊添加對登錄模塊的訂閱,當登錄模塊發生改變時,通知各個訂閱了登錄模塊的模塊。代碼如下:
// 登錄模塊js
// 登錄成功後,發佈“loginSucc”登錄成功消息,並傳遞data數據
login.succ(data=> {
login.trigger('loginSucc', data)
})
// header模塊js
// 訂閱“loginSucc”登錄成功消息
login.listen('loginSucc', () => {
header.setAvatar(data.avatar)
})
// nav模塊js
// 訂閱“loginSucc”登錄成功消息
login.listen('loginSucc', () => {
nav.setAvatar(data.avatar)
})
上述代碼用觀察者模式重構了網站登錄功能,後續不管新增多少業務模塊,依賴登錄功能,都只需要在模塊內新增對登錄成功的訂閱,無需再改動登錄模塊。
3. 雙向數據綁定
雙向數據綁定也可以通過觀察者模式實現。
雙向指的是視圖 view
和模型 model
,當視圖發生改變時,模型也發生變化,同樣,當模型發生改變,視圖也跟着同步變化。
分爲以下幾個步驟實現:
3.1 新建發佈-訂閱對象
新建一個發佈-訂閱對象,用於發佈消息,訂閱消息。
subscribe
:訂閱函數,當其他對象添加訂閱消息時,將回調函數push
進callbacks
對象數組中;publish
:發佈函數,當發佈消息時,觸發callbacks
中該消息對應的callback
.
const Pubsub = {
subscribe: function (ev, callback) {
this._callbacks || (this._callbacks = {});
(this._callbacks[ev] || (this._callbacks[ev] = [])).push(callback);
},
publish: function () {
const args = [...arguments]
const ev = args.shift()
if (!this._callbacks) return
if (!this._callbacks[ev]) return
this._callbacks[ev].forEach(callback => {
callback(...args)
})
}
}
3.2 ui
更新
3.2.1 發佈 ui
更新消息
註冊 document
的 keyup
/ change
事件,當激活事件的 dom
元素擁有 data-bind
屬性時,說明 ui
正在更新,發佈 ui
更新消息通知訂閱者。
function eventHander (e) {
const { target } = e
const { value: propValue } = target
const propNameWhole = target.getAttribute('data-bind')
if (propNameWhole) {
// 發佈ui更新消息
Pubsub.publish('ui-update-event', { propNameWhole, propValue })
}
}
console.log(document.addEventListener)
document.addEventListener('change', eventHander, false)
document.addEventListener('keyup', eventHander, false)
3.2.2 訂閱 model
更新消息
所有包含 data-bind
屬性的 dom
元素,訂閱 model
更新消息,當 model
更新時,ui
將會收到通知。
// 訂閱model更新消息,更新後所有符合條件的dom節點都會收到通知,進行更新
Pubsub.subscribe('model-update-event', function ({propNameWhole, propValue}) {
const elements = document.querySelectorAll(`[data-bind="${propNameWhole}"]`)
elements.forEach(element => {
const elementTagName = element.tagName.toLowerCase()
const formTypeTagNames = ['input', 'select', 'textarea']
if (formTypeTagNames.includes(elementTagName)) {
element.value = propValue
} else {
element.innerHTML = propValue
}
})
})
3.3 model
更新
3.3.1 訂閱 ui
更新消息
訂閱 ui
更新消息,當 ui
更新時,觸發 modal
更新。
class Bind {
constructor () {
this.modelName = ''
}
initModel ({ modelName }) {
this.modelName = modelName
// 訂閱ui更新消息
Pubsub.subscribe('ui-update-event', ({propNameWhole, propValue}) => {
const [ , _propName] = propNameWhole.split('.')
this.updateModalData(_propName, propValue)
})
}
// xxx省略xxx
updateModalData (propName, propValue) {
const propNameWhole = `${this.modelName}.${propName}`
// 發佈model更新消息
Pubsub.publish('model-update-event', { propNameWhole, propValue });
}
}
3.3.2 發佈model更新消息
model
更新時,發佈 model
更新消息,此時,訂閱了 model
更新消息的 ui
,將得到通知。
class Bind {
constructor () {
this.modelName = ''
}
// xxx省略xxx
loadModalData (modelData) {
for (let propName in modelData) {
this.updateModalData(propName, modelData[propName])
}
}
updateModalData (propName, propValue) {
const propNameWhole = `${this.modelName}.${propName}`
// 發佈model更新消息
Pubsub.publish('model-update-event', { propNameWhole, propValue });
}
}
3. 總結
從上文的實際場景例子可見,觀察者模式建立了一套觸發機制,幫助我們完成更松耦合的代碼編寫。但是也不能過度使用,否則會導致程序難以追蹤和理解。
二、裝飾器模式
1. 什麼是裝飾器模式
簡單來說,裝飾器模式就是給對象動態增加功能。
有一個鴨子對象,它會發聲 makeVoice
, 會睡覺 sleep
,但是因爲它還太小,不會走路,代碼如下:
const duck = {
makeVoice: () => {
console.log('我會嘎嘎嘎啦')
},
sleep: () => {
console.log('誰又不會睡覺呢')
},
init: function () {
this.makeVoice()
this.sleep()
}
}
duck.init()
當它 3 個月大的時候,突然學會走路 walk
了,這個時候,怎麼在代碼中,爲鴨子 duck
添加走路 walk
功能呢?大多數情況下,我們會選擇直接修改鴨子 duck
方法,代碼如下:
const duck = {
makeVoice: () => {
console.log('我會嘎嘎嘎啦')
},
sleep: () => {
console.log('誰又不會睡覺呢')
},
walk: () => {
console.log('哈哈哈,我會走路啦')
},
init: function () {
this.makeVoice()
this.sleep()
this.walk()
}
}
duck.init()
快樂的時光總是短暫的,鴨子越長越大,功能也越來越多。有一天,你請假出去玩,拜託朋友幫你照顧這隻鴨子,不巧,鴨子要下蛋了,朋友需要幫鴨子添加一個下蛋的功能,這就有點麻煩了因爲這是他第一次照顧這隻鴨子,他擔心如果直接在鴨子內部添加方法會影響到什麼。
於是他想到了一個好方法,不直接修改鴨子內部,而是通過一個外部函數,引用這個鴨子,併爲外部函數添加下蛋的功能。
代碼如下:
const before = function (fn, beforeFn) {
return function () {
beforeFn.apply(this, arguments)
return fn.apply(this, arguments)
}
}
const after = function (fn, afterFn) {
return function () {
const __ = fn.apply(this, arguments)
afterFn.apply(this, arguments)
return __
}
}
const duck = {
makeVoice: function () {
console.log('我會嘎嘎嘎啦')
},
sleep: function () {
console.log('誰又不會睡覺呢')
},
walk: function () {
console.log('哈哈哈,我會走路啦')
},
init: function () {
this.makeVoice()
this.sleep()
this.walk()
}
}
after(duck.init, function egg () {
console.log('生蛋快樂~')
}).apply(duck)
這就是裝飾器模式,動態的爲鴨子添加功能,而不直接修改鴨子本身。
2. 實際場景
1. 數據上報
自定義事件的數據上報一般都依賴於點擊事件,那麼這個點擊事件既要承擔原本的功能,又要承擔數據上報的功能。
1.1 普通做法
先上代碼:
const loginBtnClick = () => {
console.log('去登錄')
console.log('去上報')
}
好像沒毛病,這樣的代碼中項目中隨處可見,逃避(面向過程編程)雖可恥但有用。
1.2 裝飾器模式做法
可以通過裝飾器模式來重構上述代碼,將職責劃分更細,代碼松耦合,可複用性更高。
const after = function (fn, afterFn) {
return function () {
const __ = fn.apply(this, arguments)
afterFn.apply(this, arguments)
return __
}
}
const showLogin = function () {
console.log('去登錄')
}
const log = function () {
console.log('去上報')
}
const loginBtnClick = after(showLogin, log)
loginBtnClick()
2. 動態增加參數
一個常規的 ajax
請求參數包括 type
/ url
/ param
,當突發一個特殊情況,需要在 ajax
的參數中,新增一個 token
參數。
2.1 普通做法
先上代碼:
const ajax = function (type, url, param) {
// 新增token參數
param.token = 'xxx'
// ...ajax請求...省略
}
好了,又一次違反開放-封閉原則,直接修改了 ajax
函數內部.
2.2 裝飾器做法
通過裝飾器模式,在 ajax
調用之前,爲 ajax
增加 token
參數,代碼如下:
const before = function (fn, beforeFn) {
return function () {
beforeFn.apply(this, arguments)
return fn.apply(this, arguments)
}
}
let ajax = function (type, url, param) {
console.log(arguments)
// ...ajax請求...省略
}
ajax = before(ajax, function (type, url, param) {
console.log(param)
param.token = 'xxx'
})
ajax('type', 'url', {name: 'tj'})
這樣做可以減少 ajax
函數的職責,提高了 ajax
函數的可複用性,
3. 總結
本文通過給鴨子函數動態增加功能、數據上報、動態增加參數 3 個例子,講述了裝飾器模式的應用場景及帶給我們的好處。
裝飾器模式,讓對象更加穩定,且易於複用。而不穩定的功能,則可以在個性化定製時進行動態添加。
三、職責鏈模式
1. 什麼是職責鏈模式
職責鏈模式就是當一個對象 a
,有多種可能的請求對象 b
、c
、d
、e
時,我們爲 b
、c
、d
、e
分別定義一個職責,組成一條職責鏈,這樣 a
只需要找到 b
發起請求,然後沿着職責鏈繼續請求,直到找到一個對象來處理 a
。
女孩子們都喜歡結伴喫飯,我現在要找一個人一起喫飯,代碼如下:
嗯.....女程序員確實是這樣,喫個飯也要寫代碼發請求的。
const [ astrid, brooke, calliope ] = [{
name: 'astrid',
requirement: '我要喫湘菜'
},{
name: 'brooke',
requirement: '我要找10個人一起喫飯'
},{
name: 'calliope',
requirement: '我要和男朋友一起喫飯'
}]
// 是否滿足Astrid的要求
function isSatisfyAstrid (user) {
// ... 省略...
}
// 是否滿足Brooke的要求
function isSatisfyBrooke (user) {
// ... 省略...
}
// 是否滿足Calliope的要求
function isSatisfyCalliope (user) {
// ... 省略...
}
function eatDinner () {
if (isSatisfyAstrid()) {
console.log(`我可以和 astrid 一起喫晚飯啦`)
} else if (isSatisfyBrooke()) {
console.log(`我可以和 brooke 一起喫晚飯啦`)
} else if (isSatisfyCalliope()) {
console.log(`我可以和 calliope 一起喫晚飯啦`)
} else {
console.log(`哎呀,我要一個人喫晚飯啦`)
}
}
由於 astrid
、brooke
、calliope
喫晚飯的要求不同,我需要一個個去發起晚餐請求,直到找到答應和我一起喫晚飯的人。
在這裏,我假設 astrid
的要求是要喫湘菜,brooke
的要求是要找 10 個人湊一桌一起喫,calliope
的要求是隻想和男朋友一起喫飯。
上述代碼用 if-else
的做法非常死板,假如我又多了個朋友 davi
,我必須再次修改 eatDinner
方法,違反了開放-封閉原則,不易於維護。
下面使用職責鏈來優化上述代碼,代碼如下:
// 給每個人定義一個職責
const chainOrderA = new Chain(isSatisfyAstrid)
const chainOrderB = new Chain(isSatisfyBrooke)
const chainOrderC = new Chain(isSatisfyCalliope)
// 設置一下職責鏈的順序
chainOrderA.setNextSuccessor(chainOrderB)
chainOrderB.setNextSuccessor(chainOrderC)
// 發起請求,這時我只需要向職責鏈上的第一個人請求
function eatDinner () {
chainOrder.passRequest() // 發起請求
}
將職責作爲轉入 Chain
函數,並通過 setNextSuccessor
定義該職責的下一個職責函數,組成一條 chainOrderA
-> chainOrderB
-> chainOrderC
職責鏈,這時,我只需要向 astrid
發起請求,如果請求失敗,將會沿着職責鏈繼續請求,直到找到和我一起喫晚飯的人。
下面將講述在實際場景中怎麼使用職責鏈模式,怎麼實現 Chain
方法,請繼續往下看。
2. 實際場景
1. 618 預售商品訂單
電商網站免不得會推出商品預售活動,假設在 618 之前,預付 500 定金,可獲得 100 元優惠券,預付 200 元定金,可獲得 50 優惠券,未付定金則無優惠券。618 當天的購買事件如下:
1.1 普通做法
先上代碼。
本文代碼僅舉例說明,和業務無關。
const order = function (orderType) {
if (orderType === 500) {
console.log('已預付500定金,享有100優惠券')
} else if (orderType === 200) {
console.log('已預付200定金,享有50元優惠券')
} else {
console.log('未付定金,無優惠')
}
}
order(500) // '已預付500定金,享有100優惠券'
熟悉的代碼,一長段的 if-else
判斷,不利於維護。
1.2 職責鏈模式
定義一個職責類 Chain
。
- 接收一個職責函數
fn
作爲參數; setNextSuccessor
指定該職責的下一個職責函數;passRequest
發起對職責函數fn
的請求;- 如果返回結果是
nextSuccesstor
,說明請求失敗,繼續請求職責鏈上的下一個職責函數; - 如果不是返回
nextSuccesstor
,說明找到了接收請求的對象,返回請求結果,不再繼續執行職責鏈上的下一個職責函數。
- 如果返回結果是
代碼如下:
const Chain = function(fn) {
this.fn = fn;
this.successor = null;
};
Chain.prototype.setNextSuccessor = function(successor) {
return this.successor = successor;
}
Chain.prototype.passRequest = function() {
const ret = this.fn.apply(this, arguments)
if (ret === 'nextSuccessor') {
return this.successor && this.successor.passRequest.apply(this.successor, arguments);
}
return ret;
};
然後定義職責類實例,通過 setNextSuccessor
組成職責鏈,代碼如下:
const order500 = function (orderType) {
if (orderType === 500) {
console.log('已預付500定金,享有100優惠券')
} else {
return 'nextSuccessor'
}
}
const order200 = function (orderType) {
if (orderType === 200) {
console.log('已預付200定金,享有50元優惠券')
} else {
return 'nextSuccessor'
}
}
const chainOrder500 = new Chain(order500)
const chainOrder200 = new Chain(order200)
chainOrder500.setNextSuccessor(chainOrder200)
chainOrder500.passRequest(200)
上述代碼將 chainOrder500
和 chainOrder200
組成一條職責鏈,不管用戶是哪種類型,都只需要向 chainOrder500
發起請求,如果 chainOrder500
無法處理請求,就會繼續沿着職責鏈發起請求,直到找到能處理請求的職責方法。
通過職責鏈模式,解耦了請求發送者和多個接收者之間的複雜關係,不再需要知道具體哪個接收者來接收發送的請求,只需要向職責鏈的第一個階段發起請求。
3. 總結
職責鏈模式,幫助我們管理代碼,降低發起請求和接收請求對象之間的耦合。
職責鏈模式中的節點數量和順序是可以自由變動的,可以在運行時決定鏈中包含哪些節點。
可通過 github源碼 進行實操練習。
希望能對你有所幫助,感謝閱讀❤️
· 往期精彩 ·