前言
本文主要介绍常见的观察者模式,并尽量对其使用场景进行一些经典的案例说明。本文参考《学习js设计模式》一书的电子版。
概念解释
observer观察者模式,是其中一个对象(称为主体)根据对象(观察者)维护一个对象列表,并自动通知他们对状态的任何更改。
浏览器中事件监听场景
我们最常见的观察者模式是浏览器的事件增加监听以及触发之后进行广播的例子,常见的代码可能是下面这样的。在点击事件中,我们无法获知用户什么时候会点击,但我们可以增加一个事件的监听器,当有人点击之后,我们可以收到对应的信息,并且各个信息之间不会互相影响。
<button id="demo">点击事件</button> <script> const button = document.getElementById('demo'); button.addEventListener('click',function(){ alert(1) }) button.addEventListener('click',function(){ alert(2) }) button.click() </script>
业务场景:售楼处订阅消息
假如小王想要买房,但是售楼处说没有房子了,然后小王会一次次的来询问有没有房源;这时候,售楼处给了一个方案,说如果我们有房源了,那么就会通知你。
初步探索
上面的场景其实也是一个简单的观察者模型,不妨我们用代码来模拟一下,通过下面的代码,我们可以简单实现了当售楼处有消息时执行对应的函数,得到通知。案例代码codepen地址:链接
let saleCenter = { clientList:[], listen :function(fn){ if(typeof(fn) === 'function'){ this.clientList.push(fn) } else{ throw Error("非函数类型") } }, broad:function(msg){ if(this.clientList.length === 0 ) return ; for(let i = 0,len = this.clientList.length ;i<len;i++){ this.clientList[i] && this.clientList[i](msg) } } } saleCenter.listen(function(msg){ console.log(msg) }) saleCenter.broad('有房子销售了') saleCenter.broad('有120万的房子销售了')
优化内容一:对象动态绑定订阅模型
虽然我们定义了一个这样的发布订阅模式,但是有些场景下,原来的对象是没有绑定这样的模型的,是否可以动态追加订阅模型呢?肯定是可以的,我们通过installEvent来实现这样的特性。假设我们新对象叫salesOffice,把原来的发布订阅模型抽象化,称为Event.
const installEvent = (oriObj) => { for(let p in Event){ oriObj[p] = Event[p]; } } let salesOffices = { name:'销售办公室', sales:'小航', } installEvent(salesOffices) salesOffices.broad('有房子销售了')
优化内容二:取消订阅
订阅之外的功能必然还会有取消订阅,当用户不需要的时候,我们需要在事件模型中增加对应的取消事件。这时候,我们会发现一个问题,是谁取消了订阅我们需要清楚,这时候需要在设计方案上为每个增加订阅的人添加一个唯一的key,让其可辨识。所以修改之后的Event模型是这样的。
在具体代码的编写中,我们考虑了原事件是否有人订阅、是否传入函数、以及传入函数取消对应函数的不同情况。
Event.remove = function(key,fn){ var fns = this.clientList[key]; if(!fns){return false ;} if(!fn){ fns && fns.length = 0 } for(let i = fns.length - 1;i>= 0 ; i--){ let _fn = fns[i]; if(_fn === fn){ fns.splice(i, 1); } } }
优化内容三:补发订阅信息
前面的模型的基础是我们必须先订阅然后才能看到订阅消息,那么在未订阅之前,如果没人订阅那么那些消息就都失效了无从查询。这种情况下,我们可以建立一个离线缓存栈存储,当有人订阅时,重新发布这些事件。但需要注意的一点是,重新发布只能执行一次。
那么主要的改动设计会是:增加缓存事件列表,在监听中如果发现是新增对象监听,那么把已有的发布事件全部重新发布给这个对象,在发布事件中增加对个别对象的推送参数。
let Event= { clientList:[], cacheBroadList:[], listen :function(key,fn){ if(typeof(fn) === 'function'){ if(!(key in this.clientList)){ // 之前没有这个对象 那么需要补发所有的事件 this.clientList[key] = [] } this.clientList[key].push(fn) if(this.cacheBroadList.length > 0){ for(let i = 0 ,len = this.cacheBroadList.length;i < len ;i++){ this.broad(this.cacheBroadList[i],key) } } } else{ throw Error("非函数类型") } }, broad:function(msg,watcher){ // 缓存下来所有的事件 this.cacheBroadList.push('oldMsg' + msg) // 针对所有监听对象发布 if(!watcher){ for(let p in this.clientList){ if(this.clientList[p].length === 0 ) return false; for(let i = 0,len = this.clientList[p].length ;i<len;i++){ this.clientList[p][i] && this.clientList[p][i](msg) } } }else{ // 针对特定对象发布历史消息 for(let i = 0,len = this.clientList[watcher].length ;i<len;i++){ this.clientList[watcher][i] && this.clientList[watcher][i](msg) } } } }
实用场景:网站登录
在我们常见的场景中,我们很多位置的页面显示可能依赖于同一个数据来源,那么如何保证他们的同步呢?如果有前面的基础,我们可以很容易想到发布-订阅模式,只要我对外宣布需要登录信息的订阅我们的消息机制即可。这个其实也是vue的eventBus的事件机制的核心思想。那么延伸拓展一下,其实我们在使用vuex的这种跨组件通讯方式解决方案时,也可以实现这种需求呢。
再回想一下,如果我们不这么设计,可以怎么解决呢?我们会在登录之后的代码里写具体的回调函数,然后进行执行。其实我们稍微再去分析看下,其实发布订阅模式完成的就是将回调函数进行收集,把收集动作交给用户,把收集动作的接口暴露给用户而已。然后,但条件符合时,内部实现统一的回调函数执行即可。