欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JS前端設計模式之發(fā)布訂閱模式詳解

 更新時間:2022年08月10日 11:44:56   作者:35歲就退休的老狗  
這篇文章主要為大家介紹了JS前端設計模式之發(fā)布訂閱模式詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

引言

昨天我發(fā)布了一篇關于策略模式和代理模式的文章,收到的反響還不錯,于是今天我們繼續(xù)來學習前端中常用的設計模式之一:發(fā)布-訂閱模式。

說到發(fā)布訂閱模式大家應該都不陌生,它在我們的日常學習和工作中出現的頻率簡直不要太高,常見的有EventBus、框架里的組件間通信、鑒權業(yè)務等等......話不多說,讓我們一起進入今天的學習把!!!

發(fā)布-訂閱模式又叫觀察者模式,它定義對象間的一種一對多的依賴關系 當一個對象的狀態(tài)發(fā)生改變時,所有依賴它的訂閱者都會接收到通知。發(fā)布-訂閱模式在日常應用十分廣泛(js中一般用事件模型來替代傳統(tǒng)的發(fā)布訂閱模式,如addEventListener)。那發(fā)布-訂閱者模式有啥用呢?

例子1:

我們舉個例子,小明是一個喜歡吃包子的人,于是他每天都去樓下詢問有沒有包子,如果運氣不好今天沒有包子,小明就得白跑一趟,但是啥時候有包子小明又不知道,這讓他很是困擾。那如何解決這個問題呢,這個時候發(fā)布-訂閱模式就派上用場了。假如老板把小明的電話記了下來,有包子就通知小明,這樣小明就不會白白跑一趟了。看到這個例子你有沒有覺得這種模式很眼熟,像我們的點擊事件,ajax請求的error或者success事件其實都是用了這種模式,接下來我們就用代碼來還原上面小明的場景

version1:

const baoziShop = {};//定義包子鋪
baoziShop.listenList = [];//緩存列表 存放訂閱者的回調函數
//添加訂閱者
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);
    }
}
//接下來嘗試添加監(jiān)聽者
baoziShop.listen( function (price, baoziType) { //小明訂閱消息
    console.log(`種類:${baoziType}, 價格: ${price}`)
})
baoziShop.listen( function (price, baoziType) { //小王訂閱消息
    console.log(`種類:${baoziType}, 價格: ${price}`)
})
//接下來我們嘗試發(fā)布消息
baoziShop.trigger(2, '豆沙包');//輸出:種類:豆沙包, 價格 2 
baoziShop.trigger(3, '肉包');//輸出:種類:肉包,價格 3

上面我們已經實現了一個簡單的例子,但是上面的代碼還存在著一些問題:比如訂閱者無差別接收到發(fā)布者發(fā)布的所有消息,如果小明只喜歡吃菜包,那他不應該收到上架肉包子的通知,所以我們有必要增加一個key來讓訂閱者只訂閱自己感興趣的東西,接下來我們對代碼進行一些改動:

version2:

const baoziShop = {}; //定義包子鋪
baoziShop.listenList = {}; //存放訂閱者的回調函數 注意 這里從前面的數組改成了對象
//添加訂閱者 key用來標識訂閱者
baoziShop.listen = function(key, fn) {
    if( !this.listenList[key]) {
        this.listenList[key] = [];//如果沒有訂閱過此類消息 就給該消息創(chuàng)建訂閱列表
    }
    this.listenList[key].push(fn);//將回調放入訂閱列表
}
//發(fā)布消息
baoziShop.trigger = function() {
    const key = Array.prototype.shift.call(arguments), //取出消息類型
	fns = this.listenList[key];//取出該訂閱對應的回調列表
    if(!fns || fns.length === 0) return false;//沒有訂閱則直接返回
    for(let i = 0, fn; fn = fns[i]; i++) {
        fn.apply(this, arguments) //綁定this
    }
}
//接下來我們嘗試下訂閱不同的消息
baoziShop.listen('菜包子', function(price) { //小明訂閱菜包子的消息
    console.log('價格:', price)
})
baoziShop.listen('肉包子', function(price) { //小王訂閱肉包子
    console.log('價格:', price)
})
//接下來我們發(fā)布下消息
baoziShop.trigger('菜包子', 2); //只有訂閱菜包子的小明能收到消息
baoziShop.trigger('肉包子', 3); //只有訂閱肉包子的小王能收到通知

好了,經過上面的改寫,我們已經實現了只收到自己訂閱的類型的消息的功能。那我們不妨想一下我們的代碼還有啥可以完善的功能,比如如果小明樓下有兩個包子鋪,如果小明想要在另一個包子鋪買v包子,那這段代碼就必須在另一個包子鋪的對象上復制粘貼一遍,如果只有兩個包子鋪還好,那萬一有十個包子鋪呢?是不是得寫十遍?

所以我們正確的做法應該是將發(fā)布-訂閱的功能單獨抽離出來封裝在一個通用的對象內,這樣避免重復寫同樣的代碼,那我們按著這種思路開始改寫我們的代碼

const event = {
    listenList : [], //訂閱列表
    listen: function (key, fn) {
        if( !this.listenList[key]) {
        this.listenList[key] = [];//如果沒有訂閱過此類消息 就給該消息創(chuàng)建訂閱列表
    }
    this.listenList[key].push(fn);//將回調放入訂閱列表
    },
    trigger: function() {
        const key = Array.prototype.shift.call(arguments), //取出消息類型
	fns = this.listenList[key];//取出該訂閱對應的回調列表
    if(!fns || fns.length === 0) return false;//沒有訂閱則直接返回
    for(let i = 0, fn; fn = fns[i]; i++) {
        fn.apply(this, arguments) //綁定this
    }
    }
}

可以看到,我們將發(fā)布-訂閱那部分的邏輯抽離到event對象上,后續(xù)我們就能通過event.trigger()這種形式調用,接下來我們封裝一個可以給所有對象都動態(tài)安裝發(fā)布-訂閱功能的方法,避免重復操作

const installEvent = function(obj) {
    for(let i in event) {
        obj[i] = event[i];
    }
}
//接下來我們測試下我們的代碼 
const baoziShop = {};//定義包子鋪
installEvent(baoziShop);
//接下來我們就可以訂閱和發(fā)布消息了
baoziShop.listen('菜包子', function(price) { //小明訂閱菜包子的消息
    console.log('價格:', price)
})
baoziShop.listen('肉包子', function(price) { //小王訂閱肉包子
    console.log('價格:', price)
})
baoziShop.trigger('菜包子', 2); //只有訂閱菜包子的小明能收到消息
baoziShop.trigger('肉包子', 3); //只有訂閱肉包子的小王能收到通知

有沒有發(fā)現,經過上面的改寫,我們已經可以輕松做到給每個對象都添加訂閱和發(fā)布消息,再也不用重復寫代碼了。那趁熱打鐵,我們再思考一下,能否讓我們的代碼功能更多些,比如如果有一天,小明不想吃包子了,但是小明還是會繼續(xù)收到包子鋪的消息,這讓他很煩惱,于是他想要取消之前在包子鋪的訂閱,這就引出了另一個需求,有訂閱就應該有取消訂閱的功能!

接下來我們開始改寫我們的代碼吧

//我們給我們的event對象增加一個remove的方法用來取消訂閱
event.remove = function(key, fn) {
    const fns = this.listenList[key];//取出該key對應的列表
    if(!fns) { //如果該key沒被人訂閱,直接返回
        return false;
    } if(!fn) { //如果傳入了key但是沒有對應的回調函數,則標識取消該key對應的所有訂閱?。?
        fns && (fns.length == 0)
    }else {
        for(let len = fns.length - 1; len >= 0; len --) { //反向遍歷訂閱的回調列表
            const _fn = fns[len];
            if(_fn === fn) {
                fns.splice(len, 1) ;//刪除訂閱者的回調函數
            }
        }
    }
}
//接下來我們照常給包子鋪添加一些訂閱
const baoziShop = {};
installEvent(baoziShop);
baoziShop.listen('菜包子', fn1 = function(price) { //小明訂閱消息
    console.log('價格', price);
})
baoziShop.listen('菜包子', fn2 = function(price) { //小王訂閱消息
    console.log('價格', price)
})
baoziShop.trigger('菜包子', 2);//小明和小王都收到消息
baoziShop.remove('菜包子', fn1); //刪除小明的訂閱
baoziShop.trigger('菜包子', 2);//只有小王會收到訂閱

至此,我們的系統(tǒng)已經可以添加不同的訂閱,賦予對象訂閱-發(fā)布功能,取消訂閱等等。

理論上,我們的代碼已經可以實現簡單的功能,但是還存在著下面幾個問題:

  • 每個對象都必須添加listentrigger的功能,以及分配一個listenList的訂閱列表,這其實是資源的浪費
  • 代碼的耦合度太高,就像下面這樣
//小明必須知道包子鋪的名稱才能開始訂閱
baoziShop.listen('菜包子', function(price) {
    //....
})
//如果小明要去另外的包子鋪買 就必須訂閱另一家包子鋪
baoziAnother.listen('菜包子', function(price) {
    //....
})

這樣未免有點愚蠢,我們想下現實的例子,如果我們想買包子,我們需要一家一家去和老板說嗎?不需要的,我們大可以打開美團,在美團上購買就可以了,這其中,美團就類似于中介,我們只需要告訴美團我想吃包子,并不用關心包子是從哪里來的,而賣家只需要將消息發(fā)布到美團上,不用關心誰是消費者(這里和現實有點差異,因為現實我們買東西還是要看商家評價啥的,這里只是舉個例子),所以我們可以改寫下我們的代碼

//我們嘗試改寫event對象 使其充當一個中介的角色 將發(fā)布者和訂閱者連接起來
const Event = ({
	const listenList = {};//訂閱列表
    //添加訂閱者
    const listen = function(key, fn) {
    	if( !this.listenList[key]) {
        this.listenList[key] = [];//如果沒有訂閱過此類消息 就給該消息創(chuàng)建訂閱列表
    }
    	this.listenList[key].push(fn);//將回調放入訂閱列表
	};
	//發(fā)布消息
	const trigger = function() {
         const key = Array.prototype.shift.call(arguments), //取出消息類型
		fns = this.listenList[key];//取出該訂閱對應的回調列表
    	if(!fns || fns.length === 0) return false;//沒有訂閱則直接返回
   	 	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對應的列表
    if(!fns) { //如果該key沒被人訂閱,直接返回
        return false;
    } if(!fn) { //如果傳入了key但是沒有對應的回調函數,則標識取消該key對應的所有訂閱??!
        fns && (fns.length == 0)
    }else {
        for(let len = fns.length - 1; len >= 0; len --) { //反向遍歷訂閱的回調列表
            const _fn = fns[len];
            if(_fn === fn) {
                fns.splice(len, 1) ;//刪除訂閱者的回調函數
            }
        }
    };
    return {
        listen,
        trigger,
        remove
    }
})();
//接下來我們就能用Event來實現發(fā)布-訂閱功能而不需要創(chuàng)建那么多的對象了
Event.listen('菜包子', function(price) { //小明訂閱消息
    console.log('價格:', price)
})
Event.listen('菜包子', 2);//包子鋪發(fā)布消息

經過修改,我們現在訂閱消息不再需要知道包子鋪的名稱,也不需要給每個包子鋪都創(chuàng)建一個對象,只需要統(tǒng)一通過Event對象來訂閱就好,而發(fā)布消息也是這樣的流程,這樣我們就巧妙地通過Event這個中介對象把發(fā)布者和訂閱者聯系起來了。

我們的發(fā)布訂閱模式不止可用于上面這種例子,比較常見的還有模塊間的通信(學過vue或者react的小伙伴應該都對組件間的事件響應不陌生),接下來就看看怎么使用

//例如我們在a元素發(fā)布一個消息 b元素就可以監(jiān)聽到并實施對應的操作
a.onclick = () => {
    Event.listen('onclickEvent', 'this is data')
}
//b元素接收到消息
const b = (function() {
    Event.listen('onclikcEvent', function(data) {
        console.log('這是接收到的數據', data);//輸出這是接收到的數據thisisdata
    })
})();

這種用法在我們日常開發(fā)中用到的非常多!

同樣,我們也可以把它用在有關登錄的業(yè)務上,想象這么一個需求,如果在用戶登陸后,首頁需要更新用戶推薦內容,用戶個人信息和好友列表等,那我們應該怎么做呢?

由于我們并不知道用戶啥時候會登錄,所以我們可以在登錄成功后發(fā)布登錄成功的消息,然后在需要登錄權限的地方去監(jiān)聽登錄成功的消息并做相關操作,就像下面這樣

//在登錄成功后發(fā)布消息
login().then((data:[code]) => {
    if(code === 200) {
        Event.trigger('success', code);//登錄成功后發(fā)布消息
    }
})
//用戶信息模塊監(jiān)聽并更新
Event.listen('success', function(code) => {
             refleshUserInfo();//更新用戶信息
             })

這樣,即使后面有其他模塊需要鑒權,也只需要添加對應的訂閱者就可以了,不用去改動登錄部分的代碼和邏輯,這對于代碼的健壯性是有很好的幫助的。

總結

關于發(fā)布-訂閱模式就講這么多,可以看到這種設計模式還是用處非常大的,實現難度也不大,但是也要注意一些小細節(jié),比如注意命名沖突(每個key都是唯一的,可用ES6的Symbol單獨封裝到專門文件),比如會消耗一定的內存和時間,因為你訂閱一個消息后,除非手動取消,不然訂閱者會一一直存在于內存中造成浪費等等,但是總的來說發(fā)布-訂閱模式的用處和好處還是非常多的,希望大家都可以掌握并熟練使用這種模式??!

前端常見的設計模式和使用場景

一文帶你讀懂作用域、作用鏈和this的原理

更多關于JS發(fā)布訂閱模式的資料請關注腳本之家其它相關文章!

相關文章

最新評論