乐趣区

关于设计模式:JavaScript发布订阅模式与观察者模式

公布 - 订阅模式与观察者模式

  • 公布 - 订阅模式与观察者模式
  • 1. 什么是公布 - 订阅模式
  • 2. 举例

    • 1. 创立一个公布 - 订阅管理器
    • 2. 页面
    • 3. 业务代码
  • 3. 公布 - 订阅模式与观察者模式有什么区别
  • 参考文献
  • 源码查看

1. 什么是公布 - 订阅模式

公布订阅模式罕用于异步编程,在浏览器中,咱们调用 document.body.addEventListener('click', function(){}) 就是一种这种模式的实现,这段代码是在订阅一个 body 上的点击事件,当用户点击 body 后,dom 就会公布一个类型为 click 的音讯,并且执行咱们注册的回调事件,这时咱们就能监听到用户点击行为了。

举一个企业招聘的例子,X 公司的 HR 在智联在智联招聘网站上公布一条招聘前端工程师的信息,当你登陆该网站后订阅前端招聘告诉,只有有企业公布起那段招聘信息,你都会收到音讯。

那么,你的订阅行为就是subscribe,HR 公布音讯行为就是publish, 网站就是一个音讯中介,负责将 HR 公布的信息发送到每个订阅人的手机上。

咱们总结一下订阅模式三要素

  • 1. 订阅者 -subscriber
  • 2. 公布音讯者 -publisher
  • 3. 中介公司

2. 举例

上面咱们举一个购物网站的例子,设计一个页面,蕴含告诉栏 / 购物栏 / 订单列表三个局部

当购买意见商品后,刷新告诉栏和订单列表。

在这个例子中,触发按钮 购买 公布一条音讯将商品数量增加到订单中,订阅者在收到新商品后刷新告诉栏和订单列表.

1. 创立一个公布 - 订阅管理器

蕴含三个办法

  • 1.create(): 创立实例
  • 2.publish(): 公布一条音讯
  • 3.subscribe(): 订阅音讯
/**
 * 创立订阅 - 公布模式实例
 * create()办法创立出的实例是单例模式,每个命名空间都有一个独立的单例对象。*/
const PSManager = (function () {var namespacesCache = {}
    var listenerMap = new Map();// key 为音讯类型
    /**
     * 公布音讯
     * @param {string} type   音讯类型
     * @param  {...any} args 音讯内容
     */
    var publish = function(type, ...args){if (listenerMap.get(type)) {var listeners = listenerMap.get(type);
            listeners.forEach(callback => {// 订阅池里的所有事件全副触发
                callback && callback.apply(null, args)
            });
        }
    }
    /**
     * 订阅音讯
     * @param {string} type     音讯类型
     * @param {*} callback      收到音讯后执行回调函数
     */
    var subscribe = function(type, callback){if (listenerMap.get(type)) {var listeners = listenerMap.get(type);
            listeners.push(callback)// 增加新的订阅
        } else {listenerMap.set(type, [callback])
        }
    }
    /**
     * 创立一个公布 - 订阅模式的实例
     * @param {*} namespace     命名空间,解决多个模块调用呈现抵触, 不传默认 '_default'
     * @returns 实例
     */
    var create = function(namespace = '_default'){return namespacesCache[namespace] ? namespacesCache[namespace] : {publish, subscribe}
    }
    return {publish, subscribe, create}
})()

2. 页面

<!DOCTYPE html>
<html>
<style>
    button {margin-right: 16px;}
    div {margin-bottom: 20px;}
</style>
<head>
    <meta charset="utf-8">
    <title> 公布 - 订阅模式 </title>
</head>
<body>
    <div>
        <span> 商品数:0</span>
        <span style="margin-left: 16px;"> 您刚购买了 0 个手机 </span>
    </div>
    <div>
        <span> 手机 8000 元 / 部 </span> <button> 购买 </button>
        <span> 玩具 200 元 / 个 </span> <button> 购买 </button> 
        <span> 鞋子 400 元 / 双 </span> <button> 购买 </button> 
    </div>
    <ul>
        <li> 手机 0 部,总计 0 元 </li>
        <li> 玩具 0 个,总计 0 元 </li>
        <li> 鞋子 0 双,总计 0 元 </li>
    </ul>
</body>
<script src="./index.js"></script>
</html>

3. 业务代码

// 状态管理器,存储商品数量、单价,总生产价格等信息
var State = function(){
    var state = {phone: [8000,0,0], toy: [200,0,0], shoes: [400,0,0]
    }
    return {add:function(key,count){// 增加一个商品
            state[key] === undefined ? state[key][1] = 0 : state[key][1] += count;
            return this;
        },
        getCount: function(key){// 某类商品数量
            return state[key] === undefined ? 0 : state[key][1]
        },
        getPrice: function(key){// 商品总价 = 数量 * 单价
            return state[key] === undefined ? 0 : state[key][1] * state[key][0];
        },
        sum: function(){// 总商品数
            return Object.keys(state).reduce((ret, key) => ret += state[key][1], 0);
        },
        getTip: function(key){// 提醒语
            switch(key){
                case 'phone':
                    return [` 手机 ${this.getCount(key)}部,总计 ${this.getPrice(key)}元 `, '您刚购买了 1 部手机'];
                case 'toy':
                    return [` 玩具 ${this.getCount(key)}个,总计 ${this.getPrice(key)}元 `, '您刚购买了 1 个玩具']
                case 'shoes':
                    return [` 鞋子 ${this.getCount(key)}双,总计 ${this.getPrice(key)}元 `, '您刚购买了 1 双鞋子']
                default:
                    return [];}
        }
    }
}()
// 具体业务代码
window.onload = function () {var btns = document.getElementsByTagName('button')
    var spans = document.getElementsByTagName('span')
    var lies = document.getElementsByTagName('li')
    // 公布音讯
    btns[0].onclick = function () {// 购买手机
        PSManager.create('order_list').publish('phone',1)
    }
    btns[1].onclick = function () {// 购买玩具
        PSManager.create('order_list').publish('toy',1)
    }
    btns[2].onclick = function () {// 购买鞋子
        PSManager.create('order_list').publish('shoes',1)
    }
    // 订阅音讯
    PSManager.create('order_list').subscribe('phone',function(count){lies[0].innerText = State.add('phone', count).getTip('phone')[0];// 刷新告诉栏
        spans[0].innerText = ` 总商品数:${State.sum()}`;// 刷新告诉栏
        spans[1].innerText = State.getTip('phone')[1]// 刷新订单列表
    })
    PSManager.create('order_list').subscribe('toy',function(count){lies[1].innerText = State.add('toy', count).getTip('toy')[0];
        spans[0].innerText = ` 总商品数:${State.sum()}`;
        spans[1].innerText = State.getTip('toy')[1]
    })
    PSManager.create('order_list').subscribe('shoes',function(count){lies[2].innerText = State.add('shoes', count).getTip('shoes')[0];
        spans[0].innerText = ` 总商品数:${State.sum()}`;
        spans[1].innerText = State.getTip('shoes')[1]
    })
}

3. 公布 - 订阅模式与观察者模式有什么区别

咱们把下面的 发布者 (publisher)改为 主题 (Subject), 订阅者 (Subsriber) 改为察看着者 Observer 就能够变为观察者模式了。

// 主题
var Subject = (function () {var namespacesCache = {};// 命名空间
    var observerMap = new Map();// 观察者汇合
    var registObserver = function (type, func) {observerMap.get(type) ? observerMap.get(type).push(func) : observerMap.set(type, [func])
    }
    /**
     * 勾销观察者
     * @param {string} type 
     * @param {Function} func 观察者
     */
    var removeObserver = function (type, func) {if (removeObserver.get(type)) {removeObserver.get(type) = removeObserver.get(type).filter(fn => fn !== func)
        } 
    }
    /**
     * 告诉观察者
     * @param {string} type 
     */
    var notifyObservers = function (type, ...args) {if (observerMap.get(type)) {Array.prototype.forEach.apply(observerMap.get(type), [function(callback,i,ary){callback && callback.apply(null,args);// 执行回调
            }])
        }
    }
    /**
     * 创立主题
     * @param {string} namespace 
     * @returns 
     */
    var create = function(namespace = '_default'){return namespacesCache[namespace] ? namespacesCache[namespace] : namespacesCache[namespace] = {registObserver, removeObserver,notifyObservers}
    }
    return {create}

})()

调用形式与公布 - 订阅模式一样

    btns[0].onclick = function () {// 购买手机
        Subject.create('order_list').notifyObservers('phone',1)
    }
    Subject.create('order_list').registObserver('toy',function(count){lies[1].innerText = State.add('toy', count).getTip('toy')[0];
        spans[0].innerText = ` 总商品数:${State.sum()}`;
        spans[1].innerText = State.getTip('toy')[1]
    })

因而我认为 观察者模式 === 公布 - 订阅模式

如果非要说出他们之间的区别,我认为公布 - 订阅模式比观察者模式解耦能力更强。

参考文献

  • JavaScript 设计模式与开发实际. 曾探. [D]
  • Head First 设计模式. Freeman. [D]

源码查看

  • 代码实现都放在了 github 上
退出移动版