发布-订阅模式(Observer)
发布-订阅模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
该模式无论在开发中还是现实生活中都非常常见。
比如:
vue
中的数据响应式。其中就用到数据劫持和 发布-订阅模式- 消息系统
- 生活中,订阅淘宝某件商品减价或到货时通知 等等
在没有使用 发布-订阅模式 的时候,比如最后一个例子中,我们需要定时上去看看降价了没有或到货了没有,即浪费人力也浪费时间;万一刚好上去看完关闭页面的时候,或许就到货了你不知道,等你下次登上去看的时候又没货了。
当使用 发布-订阅模式 的时候,你就不需要关心它什么时候才降价,也不需要定时去查看,当降价的时候,它就会自动通知你该物品降价了。这就是 发布-订阅模式 显而易见的优点。
常用场景:
- 常用于异步编程中
- 消息通知机制
原生中的 发布-订阅模式
在原生 addEventListener
事件函数中,就有用到 发布-订阅模式。
document.body.addEventListener('click', function () { alert('被点击了');}, false);document.body.click(); // 模拟用户点击
在这里我们不知道什么时候 body
才会被点击,所以我们订阅 body
上的 click
事件,当它被点击的时候,就会向所有的订阅者发布该消息。而且添加任何订阅者或删除订阅者都不会互相影响,即removeEventListener
自定义 发布-订阅模式
使用 发布-订阅模式 实现一个简单的降价通知系统
const shop = { depList: [], // 缓存列表 listen (key, fn) { // 添加订阅者 if (!this.depList[key]) { this.depList[key] = [] } this.depList[key].push(fn) }, trigger () { // 发布 let key = Array.prototype.shift.call(arguments) const fns = this.depList[key] if (!fns || fns.length === 0) { return false } for (let i = 0, fn; fn = fns[i++];) { fn.apply(this, arguments) } }}
shop.listen('笔记本', price => { console.log(`小明,你关注的降价到了${price}`)})
shop.listen('衣服', price => { console.log(`小红,你关注的降价到了${price}`)})
shop.trigger('笔记本', 3000)shop.trigger('衣服', 50)// 输出:// 小明,你关注的降价到了3000// 小红,你关注的降价到了50
现在已经基本实现了当他们订阅一次之后,当关注的商品的价格发生改变就会自动通知他们。如:小红关注衣服,只收到衣服的消息,而不会收到其他商品的降价消息。订阅者可以只订阅自己感兴趣的事件了。
通用发布-订阅模式
有时候多处需要用到该模式功能的时候,每次都复制粘贴一份,一来代码冗余,二来操作繁琐。所以把发布订阅的功能单独提取出来,当需要的时候为它安装即可。
const event = { clientList: [], listen (key, fn) { if (!this.clientList[key]) { this.clientList[key] = [] } this.clientList[key].push(fn) }, trigger () { let key = Array.prototype.shift.call(arguments) let fns = this.clientList[key]
if (!fns || fns.length === 0) { return false }
for (let i = 0, fn; fn = fns[i++];) { fn.apply(this, arguments) } }, /** * 移除订阅事件 * @param key 事件类型 * @param fn {Function} 需要移除的事件 * @returns {boolean} */ remove (key, fn) { const fns = this.clientList[key]
if (!fns) { return false } // 如果没有传入具体的函数,则表示把该类型的所有订阅清除 if (!fn) { fns && (fns.length = 0) } else { for (let i = fns.length - 1; i >= 0; i--) { const _fn = fns[i] if (_fn === fn) { fns.splice(1, 1) } } } }}
// 自动为对象安装发布订阅功能const installEvent = function (obj) { for (const key in event) { obj[key] = event[key] }}
例:使用上面通用发布-订阅模式例子,实现模块间的通信,如:点击按钮,span
中的数值自动更换。
vue 中的响应式类似,只是 vue 数据劫持data中的数据,在 set 的时候自动创建订阅者和发布
<button id="btn">加</button><p>当前的数是:<span id="num"></span></p>
<script> let num = 0; let spanEvent = {} installEvent(spanEvent)
spanEvent.listen('add', function () { document.getElementById('num').innerText = num })
document.getElementById('btn').addEventListener('click', function () { spanEvent.trigger('add', num++) })</script>
然后再上面那么多例子中,都是需要先订阅再进行发布消息,如果发布消息的时候还没来得急订阅怎么办?难道要丢失之前那些发布的消息吗?
我们尝试创建一个离线缓存,在订阅的时候,查看有没有离线缓存,有则先调用离线缓存再订阅。
通用全局发布订阅对象
const Event = (function () { let Event, _default = 'default'; Event = function () { let _listen, _trigger, _remove, _shift = Array.prototype.shift, _unshift = Array.prototype.unshift, namespaceCache = {}, _create, each = function (arr, fn) { let ret; for (let i = 0, l = arr.length; i < l; i++) { const n = arr[i]; // 事件 ret = fn.call(n, i, n); } return ret; }; /** * 订阅 * @param key * @param fn 事件 * @param cache 订阅事件缓存 * @private */ _listen = function (key, fn, cache) { if (!cache[key]) { cache[key] = []; } cache[key].push(fn); }; /** * 移除订阅事件 * @param key * @param cache 订阅事件缓存 * @param fn 非必传,不传则全清 * @private */ _remove = function (key, cache, fn) { if (cache[key]) { if (fn) { for (let i = cache[key].length; i >= 0; i--) { if (cache[key][i] === fn) { cache[key].splice(i, 1); } } } else { cache[key] = []; } } }; /** * 发布 * @returns {*} * @private */ _trigger = function () { // arguments = [cache, key] let cache = _shift.call(arguments), key = _shift.call(arguments), args = arguments, _self = this, stack = cache[key]; if (!stack || !stack.length) { return; }
return each(stack, function () { return this.apply(_self, args); }); }; /** * 新建命名空间 * @param namespace 非必传,如果不传则默认命名空间 * @returns {*|{one: one, trigger: trigger, listen: listen, remove: remove}} * @private */ _create = function (namespace) { let ns = namespace || _default; let cache = {}, offlineStack = [], // 离线事件 ret = { /** * 订阅,如果在订阅前就已经有发布事件,会保存到离线系统中,等订阅的时候会把离线事件先触发然后情况离线事件 * @param key * @param fn 订阅事件 * @param last 是否只执行最后一个事件 */ listen: function (key, fn, last) { _listen(key, fn, cache); if (offlineStack === null) { return; } if (last === 'last') { offlineStack.length && offlineStack.pop()(); } else { each(offlineStack, function () { this(); }); } offlineStack = null; }, one: function (key, fn, last) { _remove(key, cache); this.listen(key, fn, last); }, /** * 移除订阅事件 * @param key * @param fn 非必传,订阅事件 */ remove: function (key, fn) { _remove(key, cache, fn); }, /** * 发布 * @returns {number|*} */ trigger: function () { let fn, args, _self = this; _unshift.call(arguments, cache); args = arguments; fn = function () { return _trigger.apply(_self, args); }; if (offlineStack) { return offlineStack.push(fn); } return fn(); } }; return ns ? (namespaceCache[ns] ? namespaceCache[ns] : namespaceCache[ns] = ret) : ret; }; return { create: _create, one: function (key, fn, last) { const event = this.create(); event.one(key, fn, last); }, remove: function (key, fn) { const event = this.create(); event.remove(key, fn); }, listen: function (key, fn, last) { const event = this.create(); event.listen(key, fn, last); }, trigger: function () { const event = this.create(); event.trigger.apply(this, arguments); } }; }(); return Event;})();