JavaScript 發佈-訂閱模式

發佈-訂閱模式,看似陌生,其實否則。工做中常常會用到,例如 Node.js EventEmitter 中的 on 和 emit 方法;Vue 中的 $on$emit 方法。他們都使用了發佈-訂閱模式,讓開發變得更加高效方便。

1、 什麼是發佈-訂閱模式

1. 定義

發佈-訂閱模式實際上是一種對象間一對多的依賴關係,當一個對象的狀態發送改變時,全部依賴於它的對象都將獲得狀態改變的通知。編程

訂閱者(Subscriber)把本身想訂閱的事件註冊(Subscribe)到調度中心(Event Channel),當發佈者(Publisher)發佈該事件(Publish Event)到調度中心,也就是該事件觸發時,由調度中心統一調度(Fire Event)訂閱者註冊到調度中心的處理代碼。數組

2. 例子

好比咱們很喜歡看某個公衆號號的文章,可是咱們不知道何時發佈新文章,要不定時的去翻閱;這時候,咱們能夠關注該公衆號,當有文章推送時,會有消息及時通知咱們文章更新了。緩存

上面一個看似簡單的操做,實際上是一個典型的發佈訂閱模式,公衆號屬於發佈者,用戶屬於訂閱者;用戶將訂閱公衆號的事件註冊到調度中心,公衆號做爲發佈者,當有新文章發佈時,公衆號發佈該事件到調度中心,調度中心會及時發消息告知用戶。app

2、 如何實現發佈-訂閱模式?

1. 實現思路

  • 建立一個對象
  • 在該對象上建立一個緩存列表(調度中心)
  • on 方法用來把函數 fn 都加到緩存列表中(訂閱者註冊事件到調度中心)
  • emit 方法取到 arguments 裏第一個當作 event,根據 event 值去執行對應緩存列表中的函數(發佈者發佈事件到調度中心,調度中心處理代碼)
  • off 方法能夠根據 event 值取消訂閱(取消訂閱)
  • once 方法只監聽一次,調用完畢後刪除緩存函數(訂閱一次)

2. demo1

咱們來看個簡單的 demo,實現了 on 和 emit 方法,代碼中有詳細註釋。異步

// 公衆號對象
let eventEmitter = {};

// 緩存列表,存放 event 及 fn
eventEmitter.list = {};

// 訂閱
eventEmitter.on = function (event, fn) {
    let _this = this;
    // 若是對象中沒有對應的 event 值,也就是說明沒有訂閱過,就給 event 建立個緩存列表
    // 若有對象中有相應的 event 值,把 fn 添加到對應 event 的緩存列表裏
    (_this.list[event] || (_this.list[event] = [])).push(fn);
    return _this;
};

// 發佈
eventEmitter.emit = function () {
    let _this = this;
    // 第一個參數是對應的 event 值,直接用數組的 shift 方法取出
    let event = [].shift.call(arguments),
        fns = _this.list[event];
    // 若是緩存列表裏沒有 fn 就返回 false
    if (!fns || fns.length === 0) {
        return false;
    }
    // 遍歷 event 值對應的緩存列表,依次執行 fn
    fns.forEach(fn => {
        fn.apply(_this, arguments);
    });
    return _this;
};

function user1 (content) {
    console.log('用戶1訂閱了:', content);
};

function user2 (content) {
    console.log('用戶2訂閱了:', content);
};

// 訂閱
eventEmitter.on('article', user1);
eventEmitter.on('article', user2);

// 發佈
eventEmitter.emit('article', 'Javascript 發佈-訂閱模式');

/*
    用戶1訂閱了: Javascript 發佈-訂閱模式
    用戶2訂閱了: Javascript 發佈-訂閱模式
*/

3. demo2

這一版中咱們補充了一下 once 和 off 方法。異步編程

let eventEmitter = {
    // 緩存列表
    list: {},
    // 訂閱
    on (event, fn) {
        let _this = this;
        // 若是對象中沒有對應的 event 值,也就是說明沒有訂閱過,就給 event 建立個緩存列表
        // 若有對象中有相應的 event 值,把 fn 添加到對應 event 的緩存列表裏
        (_this.list[event] || (_this.list[event] = [])).push(fn);
        return _this;
    },
    // 監聽一次
    once (event, fn) {
        // 先綁定,調用後刪除
        let _this = this;
        function on () {
            _this.off(event, on);
            fn.apply(_this, arguments);
        }
        on.fn = fn;
        _this.on(event, on);
        return _this;
    },
    // 取消訂閱
    off (event, fn) {
        let _this = this;
        let fns = _this.list[event];
        // 若是緩存列表中沒有相應的 fn,返回false
        if (!fns) return false;
        if (!fn) {
            // 若是沒有傳 fn 的話,就會將 event 值對應緩存列表中的 fn 都清空
            fns && (fns.length = 0);
        } else {
            // 如有 fn,遍歷緩存列表,看看傳入的 fn 與哪一個函數相同,若是相同就直接從緩存列表中刪掉便可
            let cb;
            for (let i = 0, cbLen = fns.length; i < cbLen; i++) {
                cb = fns[i];
                if (cb === fn || cb.fn === fn) {
                    fns.splice(i, 1);
                    break
                }
            }
        }
        return _this;
    },
    // 發佈
    emit () {
        let _this = this;
        // 第一個參數是對應的 event 值,直接用數組的 shift 方法取出
        let event = [].shift.call(arguments),
            fns = _this.list[event];
        // 若是緩存列表裏沒有 fn 就返回 false
        if (!fns || fns.length === 0) {
            return false;
        }
        // 遍歷 event 值對應的緩存列表,依次執行 fn
        fns.forEach(fn => {
            fn.apply(_this, arguments);
        });
        return _this;
    }
};

function user1 (content) {
    console.log('用戶1訂閱了:', content);
}

function user2 (content) {
    console.log('用戶2訂閱了:', content);
}

function user3 (content) {
    console.log('用戶3訂閱了:', content);
}

function user4 (content) {
    console.log('用戶4訂閱了:', content);
}

// 訂閱
eventEmitter.on('article1', user1);
eventEmitter.on('article1', user2);
eventEmitter.on('article1', user3);

// 取消user2方法的訂閱
eventEmitter.off('article1', user2);

eventEmitter.once('article2', user4)

// 發佈
eventEmitter.emit('article1', 'Javascript 發佈-訂閱模式');
eventEmitter.emit('article1', 'Javascript 發佈-訂閱模式');
eventEmitter.emit('article2', 'Javascript 觀察者模式');
eventEmitter.emit('article2', 'Javascript 觀察者模式');

// eventEmitter.on('article1', user3).emit('article1', 'test111');

/*
    用戶1訂閱了: Javascript 發佈-訂閱模式
    用戶3訂閱了: Javascript 發佈-訂閱模式
    用戶1訂閱了: Javascript 發佈-訂閱模式
    用戶3訂閱了: Javascript 發佈-訂閱模式
    用戶4訂閱了: Javascript 觀察者模式
*/

3、 Vue 中的實現

有了發佈-訂閱模式的知識後,咱們來看下 Vue 中怎麼實現 $on$emit 的方法,直接看源碼:函數

function eventsMixin (Vue) {
    var hookRE = /^hook:/;
    Vue.prototype.$on = function (event, fn) {
        var this$1 = this;

        var vm = this;
        // event 爲數組時,循環執行 $on
        if (Array.isArray(event)) {
            for (var i = 0, l = event.length; i < l; i++) {
                this$1.$on(event[i], fn);
            }
        } else {
            (vm._events[event] || (vm._events[event] = [])).push(fn);
            // optimize hook:event cost by using a boolean flag marked at registration 
            // instead of a hash lookup
            if (hookRE.test(event)) {
                vm._hasHookEvent = true;
            }
        }
        return vm
    };

    Vue.prototype.$once = function (event, fn) {
        var vm = this;
        // 先綁定,後刪除
        function on () {
            vm.$off(event, on);
            fn.apply(vm, arguments);
        }
        on.fn = fn;
        vm.$on(event, on);
        return vm
    };

    Vue.prototype.$off = function (event, fn) {
        var this$1 = this;

        var vm = this;
        // all,若沒有傳參數,清空全部訂閱
        if (!arguments.length) {
            vm._events = Object.create(null);
            return vm
        }
        // array of events,events 爲數組時,循環執行 $off
        if (Array.isArray(event)) {
            for (var i = 0, l = event.length; i < l; i++) {
                this$1.$off(event[i], fn);
            }
            return vm
        }
        // specific event
        var cbs = vm._events[event];
        if (!cbs) {
            // 沒有 cbs 直接 return this
            return vm
        }
        if (!fn) {
            // 若沒有 handler,清空 event 對應的緩存列表
            vm._events[event] = null;
            return vm
        }
        if (fn) {
            // specific handler,刪除相應的 handler
            var cb;
            var i$1 = cbs.length;
            while (i$1--) {
                cb = cbs[i$1];
                if (cb === fn || cb.fn === fn) {
                    cbs.splice(i$1, 1);
                    break
                }
            }
        }
        return vm
    };

    Vue.prototype.$emit = function (event) {
        var vm = this;
        {
            // 傳入的 event 區分大小寫,若不一致,有提示
            var lowerCaseEvent = event.toLowerCase();
            if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
                tip(
                    "Event \"" + lowerCaseEvent + "\" is emitted in component " +
                    (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
                    "Note that HTML attributes are case-insensitive and you cannot use " +
                    "v-on to listen to camelCase events when using in-DOM templates. " +
                    "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
                );
            }
        }
        var cbs = vm._events[event];
        if (cbs) {
            cbs = cbs.length > 1 ? toArray(cbs) : cbs;
            // 只取回調函數,不取 event
            var args = toArray(arguments, 1);
            for (var i = 0, l = cbs.length; i < l; i++) {
                try {
                    cbs[i].apply(vm, args);
                } catch (e) {
                    handleError(e, vm, ("event handler for \"" + event + "\""));
                }
            }
        }
        return vm
    };
}

/***
   * Convert an Array-like object to a real Array.
   */
function toArray (list, start) {
    start = start || 0;
    var i = list.length - start;
    var ret = new Array(i);
    while (i--) {
          ret[i] = list[i + start];
    }
    return ret
}

實現思路大致相同,如上第二點中的第一條:實現思路。Vue 中實現的方法支持訂閱數組事件。this

4、 總結

1. 優勢

  • 對象之間解耦
  • 異步編程中,能夠更鬆耦合的代碼編寫

2. 缺點

  • 建立訂閱者自己要消耗必定的時間和內存
  • 雖然能夠弱化對象之間的聯繫,多個發佈者和訂閱者嵌套一塊兒的時候,程序難以跟蹤維護

5、 擴展(發佈-訂閱模式與觀察者模式的區別)

不少地方都說發佈-訂閱模式是觀察者模式的別名,可是他們真的同樣嗎?是不同的。spa

直接上圖:
clipboard.pngprototype

觀察者模式:觀察者(Observer)直接訂閱(Subscribe)主題(Subject),而當主題被激活的時候,會觸發(Fire Event)觀察者裏的事件。

發佈訂閱模式:訂閱者(Subscriber)把本身想訂閱的事件註冊(Subscribe)到調度中心(Event Channel),當發佈者(Publisher)發佈該事件(Publish Event)到調度中心,也就是該事件觸發時,由調度中心統一調度(Fire Event)訂閱者註冊到調度中心的處理代碼。

差別

  • 在觀察者模式中,觀察者是知道 Subject 的,Subject 一直保持對觀察者進行記錄。然而,在發佈訂閱模式中,發佈者和訂閱者不知道對方的存在。它們只有經過消息代理進行通訊。
  • 在發佈訂閱模式中,組件是鬆散耦合的,正好和觀察者模式相反。
  • 觀察者模式大多數時候是同步的,好比當事件觸發,Subject 就會去調用觀察者的方法。而發佈-訂閱模式大多數時候是異步的(使用消息隊列)。
  • 觀察者模式須要在單個應用程序地址空間中實現,而發佈-訂閱更像交叉應用模式。
相關文章
相關標籤/搜索