iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 5
1
Modern Web

JS Design Pattern 系列 第 5

JS Design Pattern Day05-發佈/訂閱模式 Publish/Subscribe(上)

嗨大家好今天是第五天,這次主題會切成上下兩部分

今天的主題是 發佈/訂閱模式

在“JavaScript設計模式與開發實踐”書中是說發佈/訂閱模式又叫做“觀察者模式”,但我查了一下其中好像有些差別,在書中定義了發佈/訂閱模式:

物件之間的一對多依賴關係,當一個物件狀態發生改變時,所有依賴於它的物件都得到通知。

我們可以用現實生活中的例子來說明。當你去買東西的時候,你要的款式沒了,這時候就會跟銷售業務員訂貨,你會留下你的電話讓業務員在貨到的時候打電話給你,而不是你每天都打電話問一下貨到了沒有。這樣的行為就是一種發佈/訂閱模式,實際在網頁上DOM節點綁定事件就是採用這種模式:

document.body.addEventListener('click',fn)

接下來我們將自己實作發佈/訂閱模式,我們以剛剛的買東西案子為例:

首先我們要先指定誰是發佈者(業務部門)

var saleOffices = {};

再來我們給發佈者做一個陣列,用來存放訂閱者(客戶)的資訊

saleOffices.clientList = [];

接下來實作註冊方法,有人訂閱時,把訂閱者提供的通知方法(你留的電話)放到陣列中,程式裡我們會帶入callback function當作通知的方法

saleOffices.listen = function (fn) {
    this.clientList.push(fn);
};

那當要發佈時(貨到的時候)就把陣列存放的function拿出來執行(打電話給你)

saleOffices.trigger = function () {
    for (var i = 0, fn; fn = this.clientList[i++];) {
        fn.apply(this, arguments);
    }
};

實際訂閱一下,小胖與夥伴都訂貨

saleOffices.listen(function (name, price) {
    console.log('小胖的商品到囉:', name, price+'元');
});
saleOffices.listen(function (name, price) {
    console.log('帥氣夥伴的商品到囉:', name, price+'元');
});

一行通知所有訂貨的人說貨到了

saleOffices.trigger('減肥套餐', 100000);

以上就是簡單的發佈/訂閱模式,但上面這種寫法會無法區分出來訂不同的商品,我們必須讓訂閱者只訂閱自己感興趣的訊息,所以我們再改寫一下:
還是一樣的發佈者,注意clientList已經不再是陣列了

var saleOffices = {};
saleOffices.clientList = {};

在實作註冊部分時加入key參數,代表每個訂閱者的商品名稱,那一樣商品名稱的訂閱者callback function要用陣列一起裝起來

saleOffices.listen = function (key, fn) {
    if (!this.clientList[key]) {
        this.clientList[key] = [];
    }
    this.clientList[key].push(fn);
};

那在實作發佈的時候一樣只針對這次到到的產品(key)來發佈

saleOffices.trigger = function () {
    var key = Array.prototype.shift.call(arguments);
    var fnPool = this.clientList[key];
    if (!fnPool || fnPool.length === 0) {
        return false;
    }
    for (var i = 0, fn; fn = fnPool[i++];) {
        fn.apply(this, arguments);
    }
};

那再實際訂閱一下,小胖訂了A產品以及B產品,夥伴只訂了A產品

saleOffices.listen('A', function (name, price) {
    console.log('小胖訂的A貨到了:', name, price);
});
saleOffices.listen('B', function (name, price) {
    console.log('小胖訂的B貨到了:', name, price);
});
saleOffices.listen('A', function (name, price) {
    console.log('夥伴訂的A貨到了:', name, price);
});

這時候發佈B產品貨到的話就只會通知小胖囉

saleOffices.trigger('B', 'BBB', 100000);

那我們這樣寫的時候可能會發現廠商不只一家,那假設有個saleOffices2的話難道要全部複製一次嗎?
別,我們再改改:

首先我們做個通用的物件,裏面一樣具有之前上面範例所講的clientList、listen、trigger功能(內容實作都一樣),我們額外多做了一個刪除註冊事件的功能(不是重點)

var event = {
    clientList: {},
    listen: function (key, fn) {
        if (!this.clientList[key]) {
            this.clientList[key] = [];
        }
        this.clientList[key].push(fn);
    },
    trigger: function () {
        var key = Array.prototype.shift.call(arguments);
        var fnPool = this.clientList[key];
        if (!fnPool || fnPool.length === 0) {
            return false;
        }
        for (var i = 0, fn; fn = fnPool[i++];) {
            fn.apply(this, arguments);
        }
    },
    remove : function (key, removeFn) {
        var fnPool = this.clientList[key];
        if (!fnPool || fnPool.length === 0) {
            return false;
        }
        if (!removeFn) {
            fnPool && (fnPool.length = 0);
        } else {
            for (var i = 0, fn; fn = fnPool[i]; i++) {
                if (fn === removeFn) {
                    fnPool.splice(i, 1);
                }
            }
        }
    }
};

接下來我們做一個安裝事件的功能,用意就是讓每個物件(各家廠商)都可以具有這個功能,實際作法就只是把event物件每個元素都指定到你的物件上

var installEvent = function (obj) {
    for (var i in event) {
        obj[i] = event[i];
    }
};

實際用法都跟之前的範例一樣,只是要先用installEvent"安裝"。

var saleOffices = {};
installEvent(saleOffices);
saleOffices.listen('X', function (name, price) {
    console.log('A:', name, price);
});
saleOffices.trigger('X', 'X商品', 100000);

上一篇
JS Design Pattern Day04-迭代器模式 Iterator
下一篇
JS Design Pattern Day06-發佈/訂閱模式 Publish/Subscribe(下)
系列文
JS Design Pattern 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言