最近在看設計模式的知識,並且在工做當中,作一些打點需求的時候,正好直接利用了發佈訂閱模式去實現的,這讓我對發佈訂閱這種設計模式更加的感興趣了,因而藉此機會也和你們說說這個好東東吧!html
其實在早期仍是用jq開發的時代,有不少地方,咱們都會出現發佈訂閱的影子,例若有trigger和on方法前端
再到如今的vue中,emit和on方法。他們都彷佛不約而同的自帶了發佈訂閱屬性通常,讓開發變得更加高效好用起來vue
那麼廢話很少說了,先來看看發佈訂閱模式到底何方神聖吧node
說到發佈訂閱模式,它實際上是一種對象間一對多的依賴關係(不是綜藝節目以一敵百那種),當一個對象的狀態發送改變時,全部依賴於它的對象都將獲得狀態改變的通知編程
正所謂,字數很少,不表明做用不大,那繼續來看下它的做用後端
就這兩點嗎?沒錯,點不在多,夠用就行。咱們都知道有一句很著名的諺語,羅馬不是一天建成的設計模式
固然,胖子也不是一天吃成的。因此咱們要想實現一個本身的發佈訂閱模式,之後在工做中使用,也須要一點點來的,表捉急,先從最簡單的提及api
let corp = {}; // 自定義一個公司對象 // 這裏放一個列表用來緩存回調函數 corp.list = []; // 去訂閱事件 corp.on = function (fn) { // 二話不說,直接把fn先存到列表中 this.list.push(fn); }; // 發佈事件 corp.emit = function () { // 當發佈的時候再把列表裏存的函數依次執行 this.list.forEach(cb => { cb.apply(this, arguments); }); }; // 測試用例 corp.on(function (position, salary) { console.log('你的職位是:' + position); console.log('指望薪水:' + salary); }); corp.on(function(skill, hobby) { console.log('你的技能有: ' + skill); console.log('愛好: ' + hobby); }); corp.emit('前端', 10000); corp.emit('端茶和倒水', '足球'); /* 你的職位是:前端 指望薪水:10000 你的技能有: 前端 愛好: 10000 你的職位是:端茶和倒水 指望薪水:足球 你的技能有: 端茶和倒水 愛好: 足球 */ 複製代碼
上面經過自定義事件實現了一個簡單的發佈訂閱模式,不過從打印出來的結果來看,有點小尷尬。Why?數組
由於在正常的狀況下,但願打印的是醬紫的:緩存
/*
你的職位是:前端
指望薪水:10000
你的技能有: 端茶和倒水
愛好: 足球
*/
複製代碼
之因此出現此種狀況,那是在on方法的時候一股腦的都將fn函數所有放到了列表中。然而須要的只是一個簡單的key值,就能夠解決了。讓咱們改寫一下上面的代碼
let corp = {}; // 此次換成一個對象類型的緩存列表 corp.list = {}; corp.on = function(key, fn) { // 若是對象中沒有對應的key值 // 也就是說明沒有訂閱過 // 那就給key建立個緩存列表 if (!this.list[key]) { this.list[key] = []; } // 把函數添加到對應key的緩存列表裏 this.list[key].push(fn); }; corp.emit = function() { // 第一個參數是對應的key值 // 直接用數組的shift方法取出 let key = [].shift.call(arguments), fns = this.list[key]; // 若是緩存列表裏沒有函數就返回false if (!fns || fns.length === 0) { return false; } // 遍歷key值對應的緩存列表 // 依次執行函數的方法 fns.forEach(fn => { fn.apply(this, arguments); }); }; // 測試用例 corp.on('join', (position, salary) => { console.log('你的職位是:' + position); console.log('指望薪水:' + salary); }); corp.on('other', (skill, hobby) => { console.log('你的技能有: ' + skill); console.log('愛好: ' + hobby); }); corp.emit('join', '前端', 10000); corp.emit('join', '後端', 10000); corp.emit('other', '端茶和倒水', '足球'); /* 你的職位是:前端 指望薪水:10000 你的職位是:後端 指望薪水:10000 你的技能有: 端茶和倒水 愛好: 足球 */ 複製代碼
如今來搞個通用的發佈訂閱模式實現,和剛纔的差很少,不過此次起名也要隆重些了,直接叫event吧,看代碼
let event = { list: {}, on(key, fn) { if (!this.list[key]) { this.list[key] = []; } this.list[key].push(fn); }, emit() { let key = [].shift.call(arguments), fns = this.list[key]; if (!fns || fns.length === 0) { return false; } fns.forEach(fn => { fn.apply(this, arguments); }); }, remove(key, fn) { // 這回咱們加入了取消訂閱的方法 let fns = this.list[key]; // 若是緩存列表中沒有函數,返回false if (!fns) return false; // 若是沒有傳對應函數的話 // 就會將key值對應緩存列表中的函數都清空掉 if (!fn) { fns && (fns.length = 0); } else { // 遍歷緩存列表,看看傳入的fn與哪一個函數相同 // 若是相同就直接從緩存列表中刪掉便可 fns.forEach((cb, i) => { if (cb === fn) { fns.splice(i, 1); } }); } } }; function cat() { console.log('一塊兒喵喵喵'); } function dog() { console.log('一塊兒旺旺旺'); } event.on('pet', data => { console.log('接收數據'); console.log(data); }); event.on('pet', cat); event.on('pet', dog); // 取消dog方法的訂閱 event.remove('pet', dog); // 發佈 event.emit('pet', ['二哈', '波斯貓']); /* 接收數據 [ '二哈', '波斯貓' ] 一塊兒喵喵喵 */ 複製代碼
這樣其實就實現了一個可使用的發佈訂閱模式了,其實提及來也是比較簡單的,來一塊兒屢屢思路吧
思路:
先給你們看一個,在這個新聞轉碼頁的項目中,我負責寫下面推薦流的內容(就是喜歡的人還看了那裏)。以下圖所示
圈起來的廣告部分,這裏並非我來負責的,須要另一個負責對接廣告業務的大牛來實現的。那麼,他想要在個人推薦流中插入廣告應該如何實現呢?畢竟不能把個人代碼給他,讓他再拿去開發吧,這還不夠費勁的呢,又要熟悉代碼又要開始寫廣告插入的邏輯,很折騰的,時間不應這樣的浪費掉
因而就用到了發佈訂閱模式了,我這邊不須要關注廣告插入的邏輯。我仍是我,是顏色不同的煙火,哈哈哈,扯遠了
溝通後,我只須要把用戶瀏覽到哪一頁的page頁碼傳給他便可。因此我只須要在我開發的代碼中寫一句話,利用上面實現的event來表示一下
// 省略.... render() { // 我只在渲染的時候 // 把約定好的key和他須要的page頁碼傳過去就能夠了 event.emit('soAd', page); } // 省略... 複製代碼
再來看一個,朋友。打點的用途主要是記錄用戶行爲,因此在移動圖搜新版開發的時候也會加入打點的代碼,而後統計一下pv,uv,ctr等數據,那麼直接看圖說話
如圖所示,當用戶向上滑動的時候,會展現以下的內容(這纔是我要講的地方) 這裏圈中的「猜你喜歡」部分,也是經過發請求取到數據後渲染的。然而我要作的是給「猜你喜歡」加一個展示的打點。關鍵的問題就是時機,我應該何時加打點呢?很簡單,我在請求完成並渲染到頁面上的時候加這個打點就能夠了,來看一下簡單的代碼(這不是項目代碼,只是舉個栗子)
// main.js render() { // 省略... // 當渲染到頁面的時候,發送這個打點事件 // 而後另外的一個專門負責打點的模塊裏去監聽 event.emit('relatedDD', 'related'); } // log.js event.on('relatedDD', type => { console.log(type); // 'related' // monitor是個打點工具,由超級大牛開發 monitor.log({ type }, 'disp'); }); 複製代碼
上面代碼只是簡單的舉慄,若是還有對打點不瞭解的,那我就稍微簡單的描述一下
打點經常使用的就是發送一個圖片的請求,根據請求的次數來統計數據,中間會根據不一樣的參數去作統計時的區分。
如:想知道一共有多少用戶看了「猜你喜歡」的內容,在篩選數據的時候,會直接寫上type爲related
所謂栗子就舉到這裏吧,舉太多,胳膊會酸的。哈哈不過這並非結束,由於我發現node中的一個核心模塊(events)正是上面講到的發佈訂閱模式,這不是巧合,也不是演習。因而春心蕩漾了,手舞足蹈了。跟着api,那就一塊兒來實現一個,提升一下技藝吧,Let's Go!
用過node的朋友們,應該對這個模塊不陌生,能夠說這個在node中真的是很重要的模塊了,在使用後發現,這徹底是個大寫的發佈訂閱模式啊
簡直是無所不在的存在啊,那麼廢話再也不,實現依舊。先來看看如何使用吧,來個測試用例看看
/ {'失戀', [findboy, drink]} // 監聽的目的 就是爲了構造這樣一個對象 一對多的關係 on // 發佈的時候 會讓數組的函數依次執行 emit // [findboy, drink] // let EventEmitter = require('events'); // 這裏用接下來咱們寫的 let EventEmitter = require('./events'); let util = require('util'); function Girl() { } // Girl繼承EventEmitter上的方法 util.inherits(Girl, EventEmitter); // 至關於Girl.prototype.__proto__ = EventEmitter.prototype let girl = new Girl(); let drink = function (data) { console.log(data); console.log('喝酒'); }; let findboy = function () { console.log('交友'); }; girl.on('newListener', function (eventName) { // console.log('名稱: ' + eventName); }); girl.on('結婚', function() {}); girl.setMaxListeners(3); console.log(girl.getMaxListeners()); girl.once('失戀', drink); // {'失戀': [drink]} girl.once('失戀', drink); // {'失戀': [drink]} girl.prependListener('失戀', function () { console.log('before'); }); girl.once('失戀', drink); // {'失戀': [drink]} girl.emit('失戀', '1'); 複製代碼
以上代碼就是events核心模塊的使用方法,不用吝嗇,快快動手敲起來吧
下面來到了最重要也是最激動人心的時刻了,來開始實現一個EventEmitter吧
function EventEmitter() { // 用Object.create(null)代替空對象{} // 好處是無雜質,不繼承原型鏈的東東 this._events = Object.create(null); } // 默認最多的綁定次數 EventEmitter.defaultMaxListeners = 10; // 同on方法 EventEmitter.prototype.addListener = EventEmitter.prototype.on; // 返回監聽的事件名 EventEmitter.prototype.eventNames = function () { return Object.keys(this._events); }; // 設置最大監聽數 EventEmitter.prototype.setMaxListeners = function (n) { this._count = n; }; // 返回監聽數 EventEmitter.prototype.getMaxListeners = function () { return this._count ? this._count : this.defaultMaxListeners; }; // 監聽 EventEmitter.prototype.on = function (type, cb, flag) { // 默認值,若是沒有_events的話,就給它建立一個 if (!this._events) { this._events = Object.create(null); } // 不是newListener 就應該讓newListener執行如下 if (type !== 'newListener') { this._events['newListener'] && this._events['newListener'].forEach(listener => { listener(type); }); } if (this._events[type]) { // 根據傳入的flag來決定是向前仍是向後添加 if (flag) { this._events[type].unshift(cb); } else { this._events[type].push(cb); } } else { this._events[type] = [cb]; } // 監聽的事件不能超過了設置的最大監聽數 if (this._events[type].length === this.getMaxListeners()) { console.warn('警告-警告-警告'); } }; // 向前添加 EventEmitter.prototype.prependListener = function (type, cb) { this.on(type, cb, true); }; EventEmitter.prototype.prependOnceListener = function (type, cb) { this.once(type, cb, true); }; // 監聽一次 EventEmitter.prototype.once = function (type, cb, flag) { // 先綁定,調用後刪除 function wrap() { cb(...arguments); this.removeListener(type, wrap); } // 自定義屬性 wrap.listen = cb; this.on(type, wrap, flag); }; // 刪除監聽類型 EventEmitter.prototype.removeListener = function (type, cb) { if (this._events[type]) { this._events[type] = this._events[type].filter(listener => { return cb !== listener && cb !== listener.listen; }); } }; EventEmitter.prototype.removeAllListener = function () { this._events = Object.create(null); }; // 返回全部的監聽類型 EventEmitter.prototype.listeners = function (type) { return this._events[type]; }; // 發佈 EventEmitter.prototype.emit = function (type, ...args) { if (this._events[type]) { this._events[type].forEach(listener => { listener.call(this, ...args); }); } }; module.exports = EventEmitter; 複製代碼
上面咱們經過努力實現了node的核心模塊events,完成了EventEmitter的功能,可喜可賀,可喜可賀,給本身點個贊吧!
完成是完成了,可是你們仍是要反覆寫反覆推敲的,畢竟都沒有過目不忘的本領,仍是要努力的,加油,加油
哈哈,那麼最後的最後,來寫個小小的總結
優勢:
缺點:
強如發佈訂閱模式,也是勁酒雖好,不要貪杯的道理哦。過分使用的話,都會出現上述缺點的問題。不過合理開發合理利用,這都不是什麼大問題的。
好好利用這個最多見的模式吧,給你的編程帶來不小的昇華!今天就寫到這裏吧,感謝觀看了。哈哈,有緣下次再見!See U Again