<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
昨天我釋出了一篇關於策略模式和代理模式的文章,收到的反響還不錯,於是今天我們繼續來學習前端中常用的設計模式之一:釋出-訂閱模式。
說到釋出訂閱模式大家應該都不陌生,它在我們的日常學習和工作中出現的頻率簡直不要太高,常見的有EventBus
、框架裡的元件間通訊、鑑權業務等等......話不多說,讓我們一起進入今天的學習把!!!
釋出-訂閱模式又叫觀察者模式,它定義物件間的一種一對多的依賴關係 當一個物件的狀態發生改變時,所有依賴它的訂閱者都會接收到通知。釋出-訂閱模式在日常應用十分廣泛(js中一般用事件模型來替代傳統的釋出訂閱模式,如addEventListener
)。那釋出-訂閱者模式有啥用呢?
我們舉個例子,小明是一個喜歡吃包子的人,於是他每天都去樓下詢問有沒有包子,如果運氣不好今天沒有包子,小明就得白跑一趟,但是啥時候有包子小明又不知道,這讓他很是困擾。那如何解決這個問題呢,這個時候釋出-訂閱模式就派上用場了。假如老闆把小明的電話記了下來,有包子就通知小明,這樣小明就不會白白跑一趟了。看到這個例子你有沒有覺得這種模式很眼熟,像我們的點選事件,ajax請求的error
或者success
事件其實都是用了這種模式,接下來我們就用程式碼來還原上面小明的場景
const baoziShop = {};//定義包子鋪 baoziShop.listenList = [];//快取列表 存放訂閱者的回撥函數 //新增訂閱者 baoziShop.listen = function (fn) { baoziShop.listenList.push(fn) } //釋出訊息 baoziShop.trigger = function() { for(let i = 0, fn; fn = baoziShop.listenList[i++]) { fn.apply(this, arguments); } } //接下來嘗試新增監聽者 baoziShop.listen( function (price, baoziType) { //小明訂閱訊息 console.log(`種類:${baoziType}, 價格: ${price}`) }) baoziShop.listen( function (price, baoziType) { //小王訂閱訊息 console.log(`種類:${baoziType}, 價格: ${price}`) }) //接下來我們嘗試釋出訊息 baoziShop.trigger(2, '豆沙包');//輸出:種類:豆沙包, 價格 2 baoziShop.trigger(3, '肉包');//輸出:種類:肉包,價格 3
上面我們已經實現了一個簡單的例子,但是上面的程式碼還存在著一些問題:比如訂閱者無差別接收到釋出者釋出的所有訊息,如果小明只喜歡吃菜包,那他不應該收到上架肉包子的通知,所以我們有必要增加一個key
來讓訂閱者只訂閱自己感興趣的東西,接下來我們對程式碼進行一些改動:
const baoziShop = {}; //定義包子鋪 baoziShop.listenList = {}; //存放訂閱者的回撥函數 注意 這裡從前面的陣列改成了物件 //新增訂閱者 key用來標識訂閱者 baoziShop.listen = function(key, fn) { if( !this.listenList[key]) { this.listenList[key] = [];//如果沒有訂閱過此類訊息 就給該訊息建立訂閱列表 } this.listenList[key].push(fn);//將回撥放入訂閱列表 } //釋出訊息 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) }) //接下來我們釋出下訊息 baoziShop.trigger('菜包子', 2); //只有訂閱菜包子的小明能收到訊息 baoziShop.trigger('肉包子', 3); //只有訂閱肉包子的小王能收到通知
好了,經過上面的改寫,我們已經實現了只收到自己訂閱的型別的訊息的功能。那我們不妨想一下我們的程式碼還有啥可以完善的功能,比如如果小明樓下有兩個包子鋪,如果小明想要在另一個包子鋪買v包子,那這段程式碼就必須在另一個包子鋪的物件上覆制貼上一遍,如果只有兩個包子鋪還好,那萬一有十個包子鋪呢?是不是得寫十遍?
所以我們正確的做法應該是將釋出-訂閱的功能單獨抽離出來封裝在一個通用的物件內,這樣避免重複寫同樣的程式碼,那我們按著這種思路開始改寫我們的程式碼
const event = { listenList : [], //訂閱列表 listen: function (key, fn) { if( !this.listenList[key]) { this.listenList[key] = [];//如果沒有訂閱過此類訊息 就給該訊息建立訂閱列表 } 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 } } }
可以看到,我們將釋出-訂閱那部分的邏輯抽離到event
物件上,後續我們就能通過event.trigger()
這種形式呼叫,接下來我們封裝一個可以給所有物件都動態安裝釋出-訂閱功能的方法,避免重複操作
const installEvent = function(obj) { for(let i in event) { obj[i] = event[i]; } } //接下來我們測試下我們的程式碼 const baoziShop = {};//定義包子鋪 installEvent(baoziShop); //接下來我們就可以訂閱和釋出訊息了 baoziShop.listen('菜包子', function(price) { //小明訂閱菜包子的訊息 console.log('價格:', price) }) baoziShop.listen('肉包子', function(price) { //小王訂閱肉包子 console.log('價格:', price) }) baoziShop.trigger('菜包子', 2); //只有訂閱菜包子的小明能收到訊息 baoziShop.trigger('肉包子', 3); //只有訂閱肉包子的小王能收到通知
有沒有發現,經過上面的改寫,我們已經可以輕鬆做到給每個物件都新增訂閱和釋出訊息,再也不用重複寫程式碼了。那趁熱打鐵,我們再思考一下,能否讓我們的程式碼功能更多些,比如如果有一天,小明不想吃包子了,但是小明還是會繼續收到包子鋪的訊息,這讓他很煩惱,於是他想要取消之前在包子鋪的訂閱,這就引出了另一個需求,有訂閱就應該有取消訂閱的功能!
接下來我們開始改寫我們的程式碼吧
//我們給我們的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);//只有小王會收到訂閱
至此,我們的系統已經可以新增不同的訂閱,賦予物件訂閱-釋出功能,取消訂閱等等。
理論上,我們的程式碼已經可以實現簡單的功能,但是還存在著下面幾個問題:
listen
和trigger
的功能,以及分配一個listenList
的訂閱列表,這其實是資源的浪費//小明必須知道包子鋪的名稱才能開始訂閱 baoziShop.listen('菜包子', function(price) { //.... }) //如果小明要去另外的包子鋪買 就必須訂閱另一家包子鋪 baoziAnother.listen('菜包子', function(price) { //.... })
這樣未免有點愚蠢,我們想下現實的例子,如果我們想買包子,我們需要一家一家去和老闆說嗎?不需要的,我們大可以開啟美團,在美團上購買就可以了,這其中,美團就類似於中介,我們只需要告訴美團我想吃包子,並不用關心包子是從哪裡來的,而賣家只需要將訊息釋出到美團上,不用關心誰是消費者(這裡和現實有點差異,因為現實我們買東西還是要看商家評價啥的,這裡只是舉個例子),所以我們可以改寫下我們的程式碼
//我們嘗試改寫event物件 使其充當一箇中介的角色 將釋出者和訂閱者連線起來 const Event = ({ const listenList = {};//訂閱列表 //新增訂閱者 const listen = function(key, fn) { if( !this.listenList[key]) { this.listenList[key] = [];//如果沒有訂閱過此類訊息 就給該訊息建立訂閱列表 } this.listenList[key].push(fn);//將回撥放入訂閱列表 }; //釋出訊息 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來實現釋出-訂閱功能而不需要建立那麼多的物件了 Event.listen('菜包子', function(price) { //小明訂閱訊息 console.log('價格:', price) }) Event.listen('菜包子', 2);//包子鋪釋出訊息
經過修改,我們現在訂閱訊息不再需要知道包子鋪的名稱,也不需要給每個包子鋪都建立一個物件,只需要統一通過Event
物件來訂閱就好,而釋出訊息也是這樣的流程,這樣我們就巧妙地通過Event
這個中介物件把釋出者和訂閱者聯絡起來了。
我們的釋出訂閱模式不止可用於上面這種例子,比較常見的還有模組間的通訊(學過vue
或者react
的小夥伴應該都對元件間的事件響應不陌生),接下來就看看怎麼使用
//例如我們在a元素髮佈一個訊息 b元素就可以監聽到並實施對應的操作 a.onclick = () => { Event.listen('onclickEvent', 'this is data') } //b元素接收到訊息 const b = (function() { Event.listen('onclikcEvent', function(data) { console.log('這是接收到的資料', data);//輸出這是接收到的資料thisisdata }) })();
這種用法在我們日常開發中用到的非常多!
同樣,我們也可以把它用在有關登入的業務上,想象這麼一個需求,如果在使用者登陸後,首頁需要更新使用者推薦內容,使用者個人資訊和好友列表等,那我們應該怎麼做呢?
由於我們並不知道使用者啥時候會登入,所以我們可以在登入成功後釋出登入成功的訊息,然後在需要登入許可權的地方去監聽登入成功的訊息並做相關操作,就像下面這樣
//在登入成功後釋出訊息 login().then((data:[code]) => { if(code === 200) { Event.trigger('success', code);//登入成功後釋出訊息 } }) //使用者資訊模組監聽並更新 Event.listen('success', function(code) => { refleshUserInfo();//更新使用者資訊 })
這樣,即使後面有其他模組需要鑑權,也只需要新增對應的訂閱者就可以了,不用去改動登入部分的程式碼和邏輯,這對於程式碼的健壯性是有很好的幫助的。
關於釋出-訂閱模式就講這麼多,可以看到這種設計模式還是用處非常大的,實現難度也不大,但是也要注意一些小細節,比如注意命名衝突(每個key都是唯一的,可用ES6的Symbol
單獨封裝到專門檔案),比如會消耗一定的記憶體和時間,因為你訂閱一個訊息後,除非手動取消,不然訂閱者會一一直存在於記憶體中造成浪費等等,但是總的來說釋出-訂閱模式的用處和好處還是非常多的,希望大家都可以掌握並熟練使用這種模式!!
更多關於JS釋出訂閱模式的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45