不同的語言-相同的模式
最近在看設計模式的知識,而且在工作當中,做一些打點需求的時候,正好直接利用了釋出訂閱模式去實現的,這讓我對釋出訂閱這種設計模式更加的感興趣了,於是藉此機會也和大家說說這個好東東吧!
其實在早期還是用jq開發的時代,有很多地方,我們都會出現釋出訂閱的影子,例如有trigger和on方法
再到現在的vue中,emit和on方法。他們都似乎不約而同的自帶了釋出訂閱屬性一般,讓開發變得更加高效好用起來
那麼廢話不多說了,先來看看釋出訂閱模式到底何方神聖吧
釋出訂閱模式
說到釋出訂閱模式,它其實是一種物件間一對多的依賴關係(不是綜藝節目以一敵百那種),當一個物件的狀態傳送改變時,所有依賴於它的物件都將得到狀態改變的通知
正所謂,字數不多,不代表作用不大,那繼續來看下它的作用
作用
- 廣泛應用於非同步程式設計中(替代了傳遞迴調函式)
- 物件之間鬆散耦合的編寫程式碼
就這兩點嗎?沒錯,點不在多,夠用就行。我們都知道有一句很著名的諺語,羅馬不是一天建成的
當然,胖子也不是一天吃成的。所以我們要想實現一個自己的釋出訂閱模式,以後在工作中使用,也需要一點點來的,表捉急,先從最簡單的說起
自定義事件
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值取消訂閱
工作中的應用
插廣告
先給大家看一個連結,在這個新聞轉碼頁的專案中,我負責寫下面推薦流的內容(就是喜歡的人還看了那裡)。如下圖所示
圈起來的廣告部分,這裡並不是我來負責的,需要另外一個負責對接廣告業務的大牛來實現的。那麼,他想要在我的推薦流中插入廣告應該如何實現呢?畢竟不能把我的程式碼給他,讓他再拿去開發吧,這還不夠費勁的呢,又要熟悉程式碼又要開始寫廣告插入的邏輯,很折騰的,時間不該這樣的浪費掉
於是就用到了釋出訂閱模式了,我這邊不需要關注廣告插入的邏輯。我還是我,是顏色不一樣的煙火,哈哈哈,扯遠了
溝通後,我只需要把使用者瀏覽到哪一頁的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的朋友們,應該對這個模組不陌生,可以說這個在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的功能,可喜可賀,可喜可賀,給自己點個贊吧!
完成是完成了,但是大家還是要反覆寫反覆推敲的,畢竟都沒有過目不忘的本領,還是要努力的,加油,加油
哈哈,那麼最後的最後,來寫個小小的總結
總結
優點:
- 物件之間的解耦
- 非同步程式設計中,可以更鬆耦合的程式碼編寫
缺點:
- 建立訂閱者本身要消耗一定的時間和記憶體
- 多個釋出者和訂閱者巢狀一起的時候,程式難以跟蹤維護
強如釋出訂閱模式,也是勁酒雖好,不要貪杯的道理哦。過度使用的話,都會出現上述缺點的問題。不過合理開發合理利用,這都不是什麼大問題的。
好好利用這個最常見的模式吧,給你的程式設計帶來不小的昇華!今天就寫到這裡吧,感謝觀看了。哈哈,有緣下次再見!See U Again