釋出訂閱模式是JavaScript設計模式系列中 特別重要的一種,特別重要,特別重要 ···
思考一下
什麼是設計模式?
- 設計模式是 前人總結的用於解決開發過程中某類問題的方法。
什麼是設計模式系列中的釋出訂閱模式?
往下看···
理解“釋出”和“訂閱”
有些文章中介紹釋出訂閱模式是喜歡用一些案例來描述,這些案例很容易理解,但是把這些案例轉換到程式描述的過程中很容易產生暈暈的感覺。那麼在這篇文章中我們從概念的角度開始逐步分析“釋出訂閱模式”。
什麼是“釋出”?
“釋出”,一般是指釋出資訊。在現實社會中怎麼釋出資訊呢?比如用你們村裡的大喇叭廣播你馬上要結婚了,讓全村的人都知道。 你也可以寫N封婚禮邀請函,直接交給N個你想讓他們參加你婚禮的人,其他未收到邀請函的人就不知道你舉辦婚禮的事。釋出資訊的方式有很多很多 ···
什麼是“訂閱”?
“訂閱”,預訂閱覽,一般是指訂閱某種東西,比如你訂閱了一份新華日報,那麼每天某個固定時間點你就能收到郵遞員給你送來的報紙。換方式去理解就是你想定時收取某種資訊,那麼資訊就會在那個時間點送到你身邊。
我們人類這麼聰明當然會想到各種方法去釋出和訂閱自己的資訊,那麼計算機程式該怎麼“釋出”和“訂閱”它的資訊呢?
下面我們以JavaScript程式為例,模擬現實社會中的“釋出訂閱”資訊的過程。那麼這個時候就要用物件導向的思想來分析這個過程。首先我們線對“釋出訂閱”這個過程進行程式建模。
// 釋出訂閱模型
var Publisher = {
watchers: { // 已經訂閱的事件, 每個事件型別的值是一個陣列,用來存放該事件下需要觸發的所有回撥函式
'tpye1': [cb1, cb2 ...],
'type1': [cb1, cb2 ...],
}, //
addWatcher: function(type, cb) { //新增訂閱者,訂閱者其實就是新增了相應的事件及其被觸發時對應的回撥函式
// type 訂閱型別
// cb 回撥函式
// 這裡將相應的事件type以及其對應的cb存入到this.wathers物件裡面
},
removeWatcher: function(type, cb) { // 刪除訂閱者
// type 訂閱型別
// cb 回撥函式
// 這裡將相應的事件type以及其對應的cb從this.wathers物件裡面刪除
},
on: function() { // 監聽,然後對所有訂閱了該type的訂閱者釋出訊息,釋出訊息其實就是觸發對應的回撥函式
// 此處可以使用arguments屬性獲取其引數
// 這裡要觸發對應type的回撥函式
}
}
複製程式碼
為了理解起來方便,上面程式僅僅是建立的釋出訂閱模型,是不是很簡單?下面我們用Js來完善這個模型使其能夠工作。
// 釋出者類
class Publisher {
constructor() {
this.watchers = {};
}
//新增訂閱者,訂閱者其實就是新增了相應的事件及其被觸發時對應的回撥函式
addWatcher(type, cb) {
if(!this.watchers[type]) {
this.watchers[type] = []
}
this.watchers[type].push(cb);
}
// 刪除訂閱者
removeWatcher(type, cb) {
var cbs = this.watchers[type]; // 取出該型別對應的訊息集合
if(!cbs) {
return false;
}
if(!cb) {
cbs && (cbs.length = 0);
}else {
for(var i=0; i<cbs.length; i++) {
if(cb === cbs[i]) {
cbs.splice(i, 1);
}
}
}
}
// 監聽,有點程式中會用trigger名,然後對所有訂閱了該type的訂閱者釋出訊息,這個過程根據type就是觸發對應的回撥函式
on() {
var type = [].shift.call(arguments);
var cbs = this.watchers[type];
if(!cbs || cbs.length == 0) {
return false;
}
for(var i=0; i<cbs.length; i++) {
cbs[i].apply(this, arguments);
}
}
}
// 釋出者實體物件
var publishObj = new Publisher();
// 新增訂閱type為'console'
publishObj.addWatcher('console', function() {
var msg = [].shift.call(arguments);
console.log(msg);
});
// 新增訂閱type為'alert'
publishObj.addWatcher('alert', function() {
var msg = [].shift.call(arguments);
alert(msg);
});
publishObj.on('console', '觸發console 1!');
publishObj.on('console', '觸發console 2!');
publishObj.on('alert', '觸發alert!');
publishObj.removeWatcher('alert', cb2); // 注意這裡是按照地址引用的。如果傳入匿名函式則刪除不了
publishObj.on('alert', '觸發alert!');
複製程式碼
上面是釋出訂閱模式的基本程式案例,基於這個案例我們可以擴充出很多常見的應用,請看下文。
釋出訂閱模式應用案例
Vue - EventBus
《面試官:既然React/Vue可以用Event Bus進行元件通訊,你可以實現下嗎?》 這篇文章中作者由淺入深的介紹了實現EventBus的思路,並給出了相應JavaScript實現程式。我們仔細分析程式碼就能發現EventBus的實現就是基於釋出訂閱模式。下面我們引用一下該文章中的程式,簡單做了下改造(將prototype
上方法的實現直接放從class內部)。
提前宣告: 我們沒有對傳入的引數進行及時判斷而規避錯誤,僅僅對核心方法進行了實現。
class EventEmeitter {
constructor() {
this.events = this.events || new Map();
this.maxListeners = this.maxListeners || 10;
}
// 監聽,然後對所有訂閱了該type的訂閱者釋出訊息,與上面不同的是這裡的type後面為引數非回撥函式
emit(type, ...args) {
let handler;
handler = this.events.get(type);
if(Array.isArray(handler)) {
for(let i=0; i<handler.length;i++) {
if(args.length > 0) {
handler[i].apply(this, args);
}else {
handler[i].call(this);
}
}
}else {
if(args.length > 0) {
handler.apply(this, args);
}else {
handler.call(this);
}
}
}
// 新增訂閱事件型別
addListener(type, callback) {
const handler = this.emit.get(type);
if(!handler) {
this.events.set(type, callback)
}else if(handler && typeof handler === 'function') {
this.events.set(type, [handler, callback]);
}else {
handler.push(callback);
}
}
// 刪除訂閱事件型別
removeListener(type, callback) {
var handler = this.events.get(type);
if(handler && typeof handler === 'function') {
this.events.delete(type, callback);
}else {
let position;
for(let i=0; i<handler.length; i++) {
if(handler[i] === callback) {
position = i;
}else {
position = -1;
}
}
if(position !== -1) {
handler.splice(i, 1);
if(handler.length == 1) {
this.events.set(type, handler[0])
}
}else {
return this;
}
}
}
}
複製程式碼
EventBus實現的過程基本和上文中介紹的釋出訂閱模式思路一致,僅僅是具體業務處理邏輯不同。
Vue - 雙向繫結/資料劫持--釋出訂閱
先看一下Vue實現雙向資料繫結的程式,其主要思想是observer每個物件的屬性,新增到訂閱器dep中,當資料發生變化的時候發出notice通知。 相關原始碼(為方便閱讀已經去掉flow部分)如下:(作者採用的是ES6+flow寫的,程式碼在src/core/observer/index.js模組裡面)。
export function defineReactive(obj, key, val, customSetter, shallow) {
const dep = new Dep(); // 建立訂閱物件
const property = Object.getOwnPropertyDescriptor(obj, key); // 獲取當前物件自身屬性描述,返回值為物件(有多個描述屬性),原型上屬性無法獲取
if(property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val); // 建立一個觀察者物件
Object.defineProperty(obj, key, {
enumerable: true,
configurale: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if(Dep.target) {
dep.depend()
if(childOb) {
childOb.dep.depend()
if(Array.isArray(value)) {
dependArray(value)
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if(newVal === value || (newVal !== newVal && value !== value)) {
return
}
if(ProcessingInstruction.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
if(setter) {
setter.call(obj, newVal)
}else {
val = newVal
}
childOb = !shallow && observe(newVal) // 繼續監聽新的屬性值
dep.notify() // 這個是真正劫持的目的,要對訂閱者釋出通知了
}
});
}
複製程式碼
上面程式是雙向資料繫結/資料劫持的部分,下面我們看一下訂閱者物件也就是Dep
的實現原始碼。
export default class Dep {
constructor() {
this.id = uid++;
this.subs = []
}
// 新增訂閱
addSub(sub) {
this.subs.push(sub);
}
// 刪除訂閱
removeSub(sub) {
remove(this.subs, sub)
}
//
depend() {
if(Dep.target) {
Dep.target.addDep(this)
}
}
// 釋出訊息
notify() {
const subs = this.subs.slice();
if(ProcessingInstruction.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for(let i=0, l=subs.length; i<l; i++) {
subs[i].update()
}
}
}
複製程式碼
vue中釋出訂閱模式應用在上文中做了簡單介紹,後續會有文章專門介紹vue原理。
總結
釋出訂閱模式的核心過程其實分為兩步,一是新增訂閱也就是新增監聽事件及對應的方法,二是釋出訊息也就是根據事件型別觸發相應的方法。
so,你可以根據釋出訂閱模式的原理聯想到更多的實際業務問題嗎?
參考文章
Javascript設計模式-超詳細筆記
釋出-訂閱模式
面試官:既然React/Vue可以用Event Bus進行元件通訊,你可以實現下嗎?