JS前端設(shè)計(jì)模式之發(fā)布訂閱模式詳解
引言
昨天我發(fā)布了一篇關(guān)于策略模式和代理模式的文章,收到的反響還不錯(cuò),于是今天我們繼續(xù)來(lái)學(xué)習(xí)前端中常用的設(shè)計(jì)模式之一:發(fā)布-訂閱模式。
說(shuō)到發(fā)布訂閱模式大家應(yīng)該都不陌生,它在我們的日常學(xué)習(xí)和工作中出現(xiàn)的頻率簡(jiǎn)直不要太高,常見(jiàn)的有EventBus、框架里的組件間通信、鑒權(quán)業(yè)務(wù)等等......話不多說(shuō),讓我們一起進(jìn)入今天的學(xué)習(xí)把!!!
發(fā)布-訂閱模式又叫觀察者模式,它定義對(duì)象間的一種一對(duì)多的依賴關(guān)系 當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴它的訂閱者都會(huì)接收到通知。發(fā)布-訂閱模式在日常應(yīng)用十分廣泛(js中一般用事件模型來(lái)替代傳統(tǒng)的發(fā)布訂閱模式,如addEventListener)。那發(fā)布-訂閱者模式有啥用呢?
例子1:
我們舉個(gè)例子,小明是一個(gè)喜歡吃包子的人,于是他每天都去樓下詢問(wèn)有沒(méi)有包子,如果運(yùn)氣不好今天沒(méi)有包子,小明就得白跑一趟,但是啥時(shí)候有包子小明又不知道,這讓他很是困擾。那如何解決這個(gè)問(wèn)題呢,這個(gè)時(shí)候發(fā)布-訂閱模式就派上用場(chǎng)了。假如老板把小明的電話記了下來(lái),有包子就通知小明,這樣小明就不會(huì)白白跑一趟了??吹竭@個(gè)例子你有沒(méi)有覺(jué)得這種模式很眼熟,像我們的點(diǎn)擊事件,ajax請(qǐng)求的error或者success事件其實(shí)都是用了這種模式,接下來(lái)我們就用代碼來(lái)還原上面小明的場(chǎng)景
version1:
const baoziShop = {};//定義包子鋪
baoziShop.listenList = [];//緩存列表 存放訂閱者的回調(diào)函數(shù)
//添加訂閱者
baoziShop.listen = function (fn) {
baoziShop.listenList.push(fn)
}
//發(fā)布消息
baoziShop.trigger = function() {
for(let i = 0, fn; fn = baoziShop.listenList[i++]) {
fn.apply(this, arguments);
}
}
//接下來(lái)嘗試添加監(jiān)聽(tīng)者
baoziShop.listen( function (price, baoziType) { //小明訂閱消息
console.log(`種類:${baoziType}, 價(jià)格: ${price}`)
})
baoziShop.listen( function (price, baoziType) { //小王訂閱消息
console.log(`種類:${baoziType}, 價(jià)格: ${price}`)
})
//接下來(lái)我們嘗試發(fā)布消息
baoziShop.trigger(2, '豆沙包');//輸出:種類:豆沙包, 價(jià)格 2
baoziShop.trigger(3, '肉包');//輸出:種類:肉包,價(jià)格 3
上面我們已經(jīng)實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的例子,但是上面的代碼還存在著一些問(wèn)題:比如訂閱者無(wú)差別接收到發(fā)布者發(fā)布的所有消息,如果小明只喜歡吃菜包,那他不應(yīng)該收到上架肉包子的通知,所以我們有必要增加一個(gè)key來(lái)讓訂閱者只訂閱自己感興趣的東西,接下來(lái)我們對(duì)代碼進(jìn)行一些改動(dòng):
version2:
const baoziShop = {}; //定義包子鋪
baoziShop.listenList = {}; //存放訂閱者的回調(diào)函數(shù) 注意 這里從前面的數(shù)組改成了對(duì)象
//添加訂閱者 key用來(lái)標(biāo)識(shí)訂閱者
baoziShop.listen = function(key, fn) {
if( !this.listenList[key]) {
this.listenList[key] = [];//如果沒(méi)有訂閱過(guò)此類消息 就給該消息創(chuàng)建訂閱列表
}
this.listenList[key].push(fn);//將回調(diào)放入訂閱列表
}
//發(fā)布消息
baoziShop.trigger = function() {
const key = Array.prototype.shift.call(arguments), //取出消息類型
fns = this.listenList[key];//取出該訂閱對(duì)應(yīng)的回調(diào)列表
if(!fns || fns.length === 0) return false;//沒(méi)有訂閱則直接返回
for(let i = 0, fn; fn = fns[i]; i++) {
fn.apply(this, arguments) //綁定this
}
}
//接下來(lái)我們嘗試下訂閱不同的消息
baoziShop.listen('菜包子', function(price) { //小明訂閱菜包子的消息
console.log('價(jià)格:', price)
})
baoziShop.listen('肉包子', function(price) { //小王訂閱肉包子
console.log('價(jià)格:', price)
})
//接下來(lái)我們發(fā)布下消息
baoziShop.trigger('菜包子', 2); //只有訂閱菜包子的小明能收到消息
baoziShop.trigger('肉包子', 3); //只有訂閱肉包子的小王能收到通知
好了,經(jīng)過(guò)上面的改寫(xiě),我們已經(jīng)實(shí)現(xiàn)了只收到自己訂閱的類型的消息的功能。那我們不妨想一下我們的代碼還有啥可以完善的功能,比如如果小明樓下有兩個(gè)包子鋪,如果小明想要在另一個(gè)包子鋪買(mǎi)v包子,那這段代碼就必須在另一個(gè)包子鋪的對(duì)象上復(fù)制粘貼一遍,如果只有兩個(gè)包子鋪還好,那萬(wàn)一有十個(gè)包子鋪呢?是不是得寫(xiě)十遍?
所以我們正確的做法應(yīng)該是將發(fā)布-訂閱的功能單獨(dú)抽離出來(lái)封裝在一個(gè)通用的對(duì)象內(nèi),這樣避免重復(fù)寫(xiě)同樣的代碼,那我們按著這種思路開(kāi)始改寫(xiě)我們的代碼
const event = {
listenList : [], //訂閱列表
listen: function (key, fn) {
if( !this.listenList[key]) {
this.listenList[key] = [];//如果沒(méi)有訂閱過(guò)此類消息 就給該消息創(chuàng)建訂閱列表
}
this.listenList[key].push(fn);//將回調(diào)放入訂閱列表
},
trigger: function() {
const key = Array.prototype.shift.call(arguments), //取出消息類型
fns = this.listenList[key];//取出該訂閱對(duì)應(yīng)的回調(diào)列表
if(!fns || fns.length === 0) return false;//沒(méi)有訂閱則直接返回
for(let i = 0, fn; fn = fns[i]; i++) {
fn.apply(this, arguments) //綁定this
}
}
}
可以看到,我們將發(fā)布-訂閱那部分的邏輯抽離到event對(duì)象上,后續(xù)我們就能通過(guò)event.trigger()這種形式調(diào)用,接下來(lái)我們封裝一個(gè)可以給所有對(duì)象都動(dòng)態(tài)安裝發(fā)布-訂閱功能的方法,避免重復(fù)操作
const installEvent = function(obj) {
for(let i in event) {
obj[i] = event[i];
}
}
//接下來(lái)我們測(cè)試下我們的代碼
const baoziShop = {};//定義包子鋪
installEvent(baoziShop);
//接下來(lái)我們就可以訂閱和發(fā)布消息了
baoziShop.listen('菜包子', function(price) { //小明訂閱菜包子的消息
console.log('價(jià)格:', price)
})
baoziShop.listen('肉包子', function(price) { //小王訂閱肉包子
console.log('價(jià)格:', price)
})
baoziShop.trigger('菜包子', 2); //只有訂閱菜包子的小明能收到消息
baoziShop.trigger('肉包子', 3); //只有訂閱肉包子的小王能收到通知
有沒(méi)有發(fā)現(xiàn),經(jīng)過(guò)上面的改寫(xiě),我們已經(jīng)可以輕松做到給每個(gè)對(duì)象都添加訂閱和發(fā)布消息,再也不用重復(fù)寫(xiě)代碼了。那趁熱打鐵,我們?cè)偎伎家幌?,能否讓我們的代碼功能更多些,比如如果有一天,小明不想吃包子了,但是小明還是會(huì)繼續(xù)收到包子鋪的消息,這讓他很煩惱,于是他想要取消之前在包子鋪的訂閱,這就引出了另一個(gè)需求,有訂閱就應(yīng)該有取消訂閱的功能!
接下來(lái)我們開(kāi)始改寫(xiě)我們的代碼吧
//我們給我們的event對(duì)象增加一個(gè)remove的方法用來(lái)取消訂閱
event.remove = function(key, fn) {
const fns = this.listenList[key];//取出該key對(duì)應(yīng)的列表
if(!fns) { //如果該key沒(méi)被人訂閱,直接返回
return false;
} if(!fn) { //如果傳入了key但是沒(méi)有對(duì)應(yīng)的回調(diào)函數(shù),則標(biāo)識(shí)取消該key對(duì)應(yīng)的所有訂閱?。?
fns && (fns.length == 0)
}else {
for(let len = fns.length - 1; len >= 0; len --) { //反向遍歷訂閱的回調(diào)列表
const _fn = fns[len];
if(_fn === fn) {
fns.splice(len, 1) ;//刪除訂閱者的回調(diào)函數(shù)
}
}
}
}
//接下來(lái)我們照常給包子鋪添加一些訂閱
const baoziShop = {};
installEvent(baoziShop);
baoziShop.listen('菜包子', fn1 = function(price) { //小明訂閱消息
console.log('價(jià)格', price);
})
baoziShop.listen('菜包子', fn2 = function(price) { //小王訂閱消息
console.log('價(jià)格', price)
})
baoziShop.trigger('菜包子', 2);//小明和小王都收到消息
baoziShop.remove('菜包子', fn1); //刪除小明的訂閱
baoziShop.trigger('菜包子', 2);//只有小王會(huì)收到訂閱
至此,我們的系統(tǒng)已經(jīng)可以添加不同的訂閱,賦予對(duì)象訂閱-發(fā)布功能,取消訂閱等等。
理論上,我們的代碼已經(jīng)可以實(shí)現(xiàn)簡(jiǎn)單的功能,但是還存在著下面幾個(gè)問(wèn)題:
- 每個(gè)對(duì)象都必須添加
listen和trigger的功能,以及分配一個(gè)listenList的訂閱列表,這其實(shí)是資源的浪費(fèi) - 代碼的耦合度太高,就像下面這樣
//小明必須知道包子鋪的名稱才能開(kāi)始訂閱
baoziShop.listen('菜包子', function(price) {
//....
})
//如果小明要去另外的包子鋪買(mǎi) 就必須訂閱另一家包子鋪
baoziAnother.listen('菜包子', function(price) {
//....
})
這樣未免有點(diǎn)愚蠢,我們想下現(xiàn)實(shí)的例子,如果我們想買(mǎi)包子,我們需要一家一家去和老板說(shuō)嗎?不需要的,我們大可以打開(kāi)美團(tuán),在美團(tuán)上購(gòu)買(mǎi)就可以了,這其中,美團(tuán)就類似于中介,我們只需要告訴美團(tuán)我想吃包子,并不用關(guān)心包子是從哪里來(lái)的,而賣(mài)家只需要將消息發(fā)布到美團(tuán)上,不用關(guān)心誰(shuí)是消費(fèi)者(這里和現(xiàn)實(shí)有點(diǎn)差異,因?yàn)楝F(xiàn)實(shí)我們買(mǎi)東西還是要看商家評(píng)價(jià)啥的,這里只是舉個(gè)例子),所以我們可以改寫(xiě)下我們的代碼
//我們嘗試改寫(xiě)event對(duì)象 使其充當(dāng)一個(gè)中介的角色 將發(fā)布者和訂閱者連接起來(lái)
const Event = ({
const listenList = {};//訂閱列表
//添加訂閱者
const listen = function(key, fn) {
if( !this.listenList[key]) {
this.listenList[key] = [];//如果沒(méi)有訂閱過(guò)此類消息 就給該消息創(chuàng)建訂閱列表
}
this.listenList[key].push(fn);//將回調(diào)放入訂閱列表
};
//發(fā)布消息
const trigger = function() {
const key = Array.prototype.shift.call(arguments), //取出消息類型
fns = this.listenList[key];//取出該訂閱對(duì)應(yīng)的回調(diào)列表
if(!fns || fns.length === 0) return false;//沒(méi)有訂閱則直接返回
for(let i = 0, fn; fn = fns[i]; i++) {
fn.apply(this, arguments) //綁定this
}
};
//取消訂閱
const remove = function(key, fn) {
const fns = this.listenList[key];//取出該key對(duì)應(yīng)的列表
if(!fns) { //如果該key沒(méi)被人訂閱,直接返回
return false;
} if(!fn) { //如果傳入了key但是沒(méi)有對(duì)應(yīng)的回調(diào)函數(shù),則標(biāo)識(shí)取消該key對(duì)應(yīng)的所有訂閱?。?
fns && (fns.length == 0)
}else {
for(let len = fns.length - 1; len >= 0; len --) { //反向遍歷訂閱的回調(diào)列表
const _fn = fns[len];
if(_fn === fn) {
fns.splice(len, 1) ;//刪除訂閱者的回調(diào)函數(shù)
}
}
};
return {
listen,
trigger,
remove
}
})();
//接下來(lái)我們就能用Event來(lái)實(shí)現(xiàn)發(fā)布-訂閱功能而不需要?jiǎng)?chuàng)建那么多的對(duì)象了
Event.listen('菜包子', function(price) { //小明訂閱消息
console.log('價(jià)格:', price)
})
Event.listen('菜包子', 2);//包子鋪發(fā)布消息
經(jīng)過(guò)修改,我們現(xiàn)在訂閱消息不再需要知道包子鋪的名稱,也不需要給每個(gè)包子鋪都創(chuàng)建一個(gè)對(duì)象,只需要統(tǒng)一通過(guò)Event對(duì)象來(lái)訂閱就好,而發(fā)布消息也是這樣的流程,這樣我們就巧妙地通過(guò)Event這個(gè)中介對(duì)象把發(fā)布者和訂閱者聯(lián)系起來(lái)了。
我們的發(fā)布訂閱模式不止可用于上面這種例子,比較常見(jiàn)的還有模塊間的通信(學(xué)過(guò)vue或者react的小伙伴應(yīng)該都對(duì)組件間的事件響應(yīng)不陌生),接下來(lái)就看看怎么使用
//例如我們?cè)赼元素發(fā)布一個(gè)消息 b元素就可以監(jiān)聽(tīng)到并實(shí)施對(duì)應(yīng)的操作
a.onclick = () => {
Event.listen('onclickEvent', 'this is data')
}
//b元素接收到消息
const b = (function() {
Event.listen('onclikcEvent', function(data) {
console.log('這是接收到的數(shù)據(jù)', data);//輸出這是接收到的數(shù)據(jù)thisisdata
})
})();
這種用法在我們?nèi)粘i_(kāi)發(fā)中用到的非常多!
同樣,我們也可以把它用在有關(guān)登錄的業(yè)務(wù)上,想象這么一個(gè)需求,如果在用戶登陸后,首頁(yè)需要更新用戶推薦內(nèi)容,用戶個(gè)人信息和好友列表等,那我們應(yīng)該怎么做呢?
由于我們并不知道用戶啥時(shí)候會(huì)登錄,所以我們可以在登錄成功后發(fā)布登錄成功的消息,然后在需要登錄權(quán)限的地方去監(jiān)聽(tīng)登錄成功的消息并做相關(guān)操作,就像下面這樣
//在登錄成功后發(fā)布消息
login().then((data:[code]) => {
if(code === 200) {
Event.trigger('success', code);//登錄成功后發(fā)布消息
}
})
//用戶信息模塊監(jiān)聽(tīng)并更新
Event.listen('success', function(code) => {
refleshUserInfo();//更新用戶信息
})
這樣,即使后面有其他模塊需要鑒權(quán),也只需要添加對(duì)應(yīng)的訂閱者就可以了,不用去改動(dòng)登錄部分的代碼和邏輯,這對(duì)于代碼的健壯性是有很好的幫助的。
總結(jié)
關(guān)于發(fā)布-訂閱模式就講這么多,可以看到這種設(shè)計(jì)模式還是用處非常大的,實(shí)現(xiàn)難度也不大,但是也要注意一些小細(xì)節(jié),比如注意命名沖突(每個(gè)key都是唯一的,可用ES6的Symbol單獨(dú)封裝到專門(mén)文件),比如會(huì)消耗一定的內(nèi)存和時(shí)間,因?yàn)槟阌嗛喴粋€(gè)消息后,除非手動(dòng)取消,不然訂閱者會(huì)一一直存在于內(nèi)存中造成浪費(fèi)等等,但是總的來(lái)說(shuō)發(fā)布-訂閱模式的用處和好處還是非常多的,希望大家都可以掌握并熟練使用這種模式!!
前端常見(jiàn)的設(shè)計(jì)模式和使用場(chǎng)景
更多關(guān)于JS發(fā)布訂閱模式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- JavaScript事件發(fā)布/訂閱模式原理與用法分析
- JavaScript實(shí)現(xiàn)與使用發(fā)布/訂閱模式詳解
- JavaScript中發(fā)布/訂閱模式的簡(jiǎn)單實(shí)例
- js 發(fā)布訂閱模式的實(shí)例講解
- JavaScript設(shè)計(jì)模式之觀察者模式(發(fā)布訂閱模式)原理與實(shí)現(xiàn)方法示例
- JavaScript設(shè)計(jì)模式之觀察者模式與發(fā)布訂閱模式詳解
- 詳解JavaScript設(shè)計(jì)模式中的享元模式
- JavaScript設(shè)計(jì)模式之單例模式應(yīng)用場(chǎng)景案例詳解
- JavaScript 設(shè)計(jì)模式 安全沙箱模式
- JavaScript設(shè)計(jì)模式之觀察者模式(發(fā)布者-訂閱者模式)
- javascript 發(fā)布-訂閱模式 實(shí)例詳解
相關(guān)文章
JS面試中你不知道的call apply bind方法及使用場(chǎng)景詳解
這篇文章主要為大家介紹了JS面試中你不知道的call apply bind方法及使用場(chǎng)景詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
微信小程序之選項(xiàng)卡的實(shí)現(xiàn)方法
這篇文章主要介紹了 微信小程序之選項(xiàng)卡的實(shí)現(xiàn)方法的相關(guān)資料,希望大家通過(guò)本文能實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下2017-09-09
JavaScript?Canvas實(shí)現(xiàn)噪點(diǎn)濾鏡回憶童年電視雪花屏
這篇文章主要為大家介紹了JavaScript?Canvas實(shí)現(xiàn)噪點(diǎn)濾鏡回憶童年電視雪花屏,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
使用純JavaScript封裝一個(gè)消息提示條功能示例詳解
這篇文章主要為大家介紹了使用純JavaScript封裝一個(gè)消息提示條功能示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
微信小程序報(bào)錯(cuò):this.setData is not a function的解決辦法
這篇文章主要介紹了微信小程序報(bào)錯(cuò):this.setData is not a function的解決辦法的相關(guān)資料,希望通過(guò)本文能幫助到大家解決這樣類似的問(wèn)題,需要的朋友可以參考下2017-09-09
Dragonfly P2P 傳輸協(xié)議優(yōu)化代碼解析
這篇文章主要為大家介紹了Dragonfly P2P 傳輸協(xié)議優(yōu)化代碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11

