🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # 发布-订阅模式 又称为观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生 改变的时,所有依赖于它的对象都将得到通知。一为时间上解耦,二为对象之间的解 耦。 ## DOM事件 DOM元素上的事件函数,就是如此。当我们为一个元素增加一个事件时,等到这个时 间触发后,就会执行我们的回调函数,同时也能够移除该事件。 ## 自定义事件 发布-订阅模式的通用实现 ```javascript 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), fns = this.clientList[key]; if(!fns || fns.length === 0){ return false; } for(var i=0; fn; fn = fns[i++]){ fn.apply(this, arguments); } }, remove: function(key, fn){ var fns = this.clientList[key]; if(!fns || fns.length === 0){ return false; } if(!fn){ fns && (fns.lengthss = 0); // 如果没有传 具的回调函数,表示需要取消key对应消息的所有订阅 }else{ for(var l = fns.length - 1; l >=0; l--){ // 反向遍历 var _fn = fns[l]; if(_fn === fn){ fns.splice(l, 1); // 删除订阅者的回调函数 } } } } }; // 再定义 个installEvent函数,这个函数可以给所有的对象都动态安装发布-订阅功能 var installEvent = function(obj){ for(var i in event){ event[i] = obj[i]; } }; var salesOffices = { ... }; installEvent(salesOffices); salesOffices.listen('event1'); salesOffices.trigger('event1'); salesOffices.remove('event1'); ``` ## 全局的发布-订阅对象 发布-订阅模式可以用一个全局的Event对象来实现,订阅者不需要了解消息来自哪个 发布者,发布者也不知道消息会推送给哪些订阅者,Event作为一个类似“中介 者”的角色,把订阅者和发布者联系起来。 ```javascript var Event = (function(){ var clientList = [], listen, trigger, remove; listen = function(key, fn){ // 订阅 if(!clientList[key]){ clientList[key] = {}; } clientList[key].push(fn); }; trigger = function(){ // 发布 var key = Array.prototype.shift.call(arguments), fns = clientList[key]; if(!fns || fns.length === 0){ return false; } for(var i=0; fn; fn = fns[i++]){ fn.apply(this, arguments); } }; remove = function(key, fn){ var fns = clientList[key]; if(!fns || fns.length === 0){ return false; } if(!fn){ fns && (fns.lengthss = 0); // 如果没有传 具的回调函数,表示需要取消key对应消息的所有订阅 }else{ for(var l = fns.length - 1; l >=0; l--){ // 反向遍历 var _fn = fns[l]; if(_fn === fn){ fns.splice(l, 1); // 删除订阅者的回调函数 } }; return { listen: listen, trigger: trigger, remove: remove } })(); Event.listen('event1'); Event.trigger('event1'); Event.remove('event1'); ``` ## 模块间通信 模块之间如果用了太多的全局发布-订阅模式来通信,那么模块与模块之间的联系就 被隐藏到了背后,我们最终会搞不清楚消息来自哪个模块,或者消息会流向哪些模 块,这又会给我们的维护带来一些麻烦,也许某个模块的作用就是暴露一些接口给其 他模块调用。 ## 必须先订阅再发布吗 在某些情况下,需要先将消息保存下来,等到有对象来订阅它的时候,再重新把消息 发布给订阅者。就如果离线消息一样。 我们需要建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者 来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数 存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这 些包装函数,也就是重新发布里面的事件。当然离线事件的生命周期只有一次。 ## 全局事件的命名冲突 ```javascript //全局作用域下的发布订阅模式 (function(){ var Event = (function{ var global = this, Modal, _default = 'default'; Event = function(){ var _listen, _trigger, _remove, _slice = Array.prototype.slice, _shift = Array.prototype.shift, _unshift = Array.prototype.unshift, namespaceCache = {}, _create, find, each = function(ary,fn){ var ret ; for(const i = 0,l = ary.length; i < l;i ++){ var n = ary[i]; ret = fn.call(n,i,n); } return ret; }; _listen = function(key,fn,cache){ if(!cache[key]){ cache[key] = []; } cache[key].push(fn); }; _remove = function(key,cache,fn){ if(cache[key]){ if(fn){ for(var i = cache[key].length;i>=0;i--){ if(cache[key] === fn){ cache[key].splice(i,1); } } }else{ cache[key] = []; } } }; _trigger = function(){ var cache = _shift.call(arguments), key = _shift.call(arguments), args = arguments, _self = this, ret, stack = cache[key]; if(!stack || !stack.length){ return; } return each(stack,function(){ return this.apply(_self,args); }); }; _create = function(namespace){ var namespace = namespace || _default; var cache = {}, offlineStack = [], ret = { 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,cache,fn); }, remove:function(key,fn){ _remove(key,cache,fn); }, trigger:function(){ var 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 namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret; }; return { create : , one: , remove: , listen:, trigger:, var event = this.create(); event.trigger.apply(this,arguments); } }(); return Event; }()); Event.create('namespace1').listen('event'); Event.create('namespace1').trigger('event'); ``` ## JavaScript 实现发布-订阅模式的便利性 - 推模型:是指在事件发生时,发布者一次性把所有更改的状态和数据都推送给订 阅者。 - 拉模型:发布者仅仅通知订阅者事件已经发生了,此外发布者要提供一些公开接口共订阅者来主动拉取数据。 刚好在JavaScript中, arguments 可以很方便地表示参数列表,所以我们一般都会选 择推模型,使用 Function.prototype.apply 方法把所有参数都推送给订阅者。