JavaScript設計模式系列--釋出訂閱模式

上古神鵬發表於2019-05-13

釋出訂閱模式是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進行元件通訊,你可以實現下嗎?

相關文章