發佈訂閱模式,在工作中它的能量超乎你的想象


不同的語言-相同的模式

最近在看設計模式的知識,而且在工作當中,做一些打點需求的時候,正好直接利用了發佈訂閱模式去實現的,這讓我對發佈訂閱這種設計模式更加的感興趣了,於是藉此機會也和大家說說這個好東東吧!

其實在早期還是用jq開發的時代,有很多地方,我們都會出現發佈訂閱的影子,例如有trigger和on方法

再到現在的vue中,emit和on方法。他們都似乎不約而同的自帶了發佈訂閱屬性一般,讓開發變得更加高效好用起來

那麼廢話不多說了,先來看看發佈訂閱模式到底何方神聖吧

發佈訂閱模式

說到發佈訂閱模式,它其實是一種對象間一對多的依賴關係(不是綜藝節目以一敵百那種),當一個對象的狀態發送改變時,所有依賴於它的對象都將得到狀態改變的通知

正所謂,字數不多,不代表作用不大,那繼續來看下它的作用

作用

  1. 廣泛應用於異步編程中(替代了傳遞迴調函數)

  2. 對象之間鬆散耦合的編寫代碼

就這兩點嗎?沒錯,點不在多,夠用就行。我們都知道有一句很著名的諺語,羅馬不是一天建成的

當然,胖子也不是一天喫成的。所以我們要想實現一個自己的發佈訂閱模式,以後在工作中使用,也需要一點點來的,表捉急,先從最簡單的說起

自定義事件

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', ['二哈', '波斯貓']);	
/*	
    接收數據	
    [ '二哈', '波斯貓' ]	
    一起喵喵喵	
*/

這樣其實就實現了一個可以使用的發佈訂閱模式了,其實說起來也是比較簡單的,來一起屢屢思路吧

思路

  • 創建一個對象(緩存列表)

  • on方法用來把回調函數fn都加到緩存列表中

  • emit方法取到arguments裏第一個當做key,根據key值去執行對應緩存列表中的函數

  • remove方法可以根據key值取消訂閱

工作中的應用

插廣告

先給大家看一個鏈接,在這個新聞轉碼頁的項目中,我負責寫下面推薦流的內容(就是喜歡的人還看了那裏)。如下圖所示

640?wx_fmt=other

圈起來的廣告部分,這裏並不是我來負責的,需要另外一個負責對接廣告業務的大牛來實現的。那麼,他想要在我的推薦流中插入廣告應該如何實現呢?

畢竟不能把我的代碼給他,讓他再拿去開發吧,這還不夠費勁的呢,又要熟悉代碼又要開始寫廣告插入的邏輯,很折騰的,時間不該這樣的浪費掉

於是就用到了發佈訂閱模式了,我這邊不需要關注廣告插入的邏輯。我還是我,是顏色不一樣的煙火,哈哈哈,扯遠了

溝通後,我只需要把用戶瀏覽到哪一頁的page頁碼傳給他即可。所以我只需要在我開發的代碼中寫一句話,利用上面實現的event來表示一下

// 省略....	
render() {	
    // 我只在渲染的時候	
    // 把約定好的key和他需要的page頁碼傳過去就可以了	
    event.emit('soAd', page);	
}	
// 省略...	

打點

再來看一個鏈接,朋友。打點的用途主要是記錄用戶行爲,所以在移動圖搜新版開發的時候也會加入打點的代碼,然後統計一下pv,uv,ctr等數據,那麼直接看圖說話

640?wx_fmt=other

如圖所示,當用戶向上滑動的時候,會展示如下的內容(這纔是我要講的地方)

640?wx_fmt=other

這裏圈中的“猜你喜歡”部分,也是通過發請求取到數據後渲染的。然而我要做的是給“猜你喜歡”加一個展現的打點。關鍵的問題就是時機,我應該什麼時候加打點呢?

很簡單,我在請求完成並渲染到頁面上的時候加這個打點就可以了,來看一下簡單的代碼(這不是項目代碼,只是舉個栗子)

// main.js	
render() {	
    // 省略...	
    // 當渲染到頁面的時候,發送這個打點事件	
    // 然後另外的一個專門負責打點的模塊裏去監聽	
    event.emit('relatedDD', 'related');	
}	

	
// log.js	
event.on('relatedDD', type => {	
    console.log(type);  // 'related'	
    	
    // monitor是個打點工具,由超級大牛開發	
    monitor.log({	
        type    	
    }, 'disp');	
});

上面代碼只是簡單的舉慄,如果還有對打點不瞭解的,那我就稍微簡單的描述一下

打點常用的就是發送一個圖片的請求,根據請求的次數來統計數據,中間會根據不同的參數去做統計時的區分。

如:想知道一共有多少用戶看了“猜你喜歡”的內容,在篩選數據的時候,會直接寫上type爲related

640?wx_fmt=other

所謂栗子就舉到這裏吧,舉太多,胳膊會酸的。哈哈

不過這並不是結束,因爲我發現node中的一個核心模塊(events)正是上面講到的發佈訂閱模式,這不是巧合,也不是演習。於是春心蕩漾了,手舞足蹈了。跟着api,那就一起來實現一個,提高一下技藝吧,Let's Go!

講真-這可是node的核心模塊

用過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

下面來到了最重要也是最激動人心的時刻了,來開始實現一個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的功能,可喜可賀,可喜可賀,給自己點個贊吧!

完成是完成了,但是大家還是要反覆寫反覆推敲的,畢竟都沒有過目不忘的本領,還是要努力的,加油,加油

哈哈,那麼最後的最後,來寫個小小的總結

總結

優點

  • 對象之間的解耦

  • 異步編程中,可以更松耦合的代碼編寫

缺點

  • 創建訂閱者本身要消耗一定的時間和內存

  • 多個發佈者和訂閱者嵌套一起的時候,程序難以跟蹤維護

強如發佈訂閱模式,也是勁酒雖好,不要貪杯的道理哦。過度使用的話,都會出現上述缺點的問題。不過合理開發合理利用,這都不是什麼大問題的。

❤️ 看完三件事

如果你覺得這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)

  2. 關注我的博客 https://github.com/SHERlocked93/blog,讓我們成爲長期關係

  3. 關注公衆號「前端下午茶」,持續爲你推送精選好文,也可以加我爲好友,隨時聊騷。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章