js设计模式之——观察者模式

338 阅读4分钟

1、什么是观察者模式?

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一目标对象,
当这个目标对象的状态发生变化时,会通知所有观察者对象,使他们能够自动更新。

2、观察者模式和发布-订阅模式的区别

  • 观察者模式在js设计模式中使用频率最高,在面试中被问到的概率最高,观察者模式‘别名’发布-订阅模式,这两者之间还是不能严格划等号的;
  • 发布-订阅模式:发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作;
  • 两者的区别主要在于是否存在第三方、发布者能否直接感知订阅者;
  • 观察者解决的是模块间的耦合问题,可以实现两个独立模块间的数据通信,但是它没有完全解决耦合问题,被观察者必须要维护一套观察者集合;
  • 发布-订阅模式,发布者完全不用感知订阅者,事件的注册和触发都发生在独立于双方的第三方平台(事件总线)上,实现了完全的解耦;
  • 两者的使用场景不同,各有各的适用场景:
  • 如果两个模块之间本身存在关联,且这种关联是稳定的、必要的,那么我们使用观察者模式就足够了。而在模块与模块之间独立性较强、且没有必要单纯为了数据通信而强行为两者制造依赖的情况下,我们往往会倾向于使用发布-订阅模式。

3、vue数据双向绑定(响应式系统)的实现

3.1 vue中的观察者模式应用
  • 在vue中,每个组件的实例都有相应的watcher实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使关联组件得以更新——这就是一个典型的观察者模式。
  • vue官网中响应式系统的流程图

data.png

3.2 实现一个简单的vue响应式代码
在vue数据双向绑定的实现逻辑里,有这样三个关键角色:
  • observer(监听器):在vue数据双向绑定的角色结构里,所谓的observer不仅是一个数据监听器,它还需要对监听到的数据进行转发,它同时还是一个发布者;
  • wathcer(订阅者):observer把数据转发给了真正的订阅者——watcher对象。watcher接收到新的数据后,会去更新视图;
  • compile(编译器):负责对每个节点元素指令进行扫描和解析,指令的数据初始化、订阅者的创建;
核心代码:
// observe方法遍历并包装对象属性
function observe(target){
    //target是个对象,则遍历它
    if(target && typeof target === 'object'){
        Object.keys(target).forEach((key)=>{
            // defineReactive方法会给目标属性装上‘监听器’
            defineReactive(target, key, target[key])
        })
    }
}
// 定义defineReactive方法
function defineReactive(target, key, val){
    // 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历
    observe(val)
    // 为当前属性安装监听器
    Object.defineProperty(target,key,{
        //可枚举
        enumerable:true,
        // 不可配置
        configurable;false,
        get:function(){
            return val
        },
        set:function(newVal){
            val = newVal
            dep.notify()
        }
    })   
}
// 实现一个订阅者
class Dep{
    constructor(){
        // 初始化订阅队列
        this.subs = []
    }
    // 增加订阅者
    addSub(sub){
        this.subs.push(sub)
    }
    // 通知订阅者
    notify(){
        this.subs.forEach((sub)=>{
            sub.update()
        })
    }
}

4、实现一个Event Bus/Event Emitter

Event Bus/Event Emitter作为全局事件总线,它起到一个沟通桥梁的作用,是一个事件中心。
实现一个Event Bus
class EventEmitter {
    constructor(){
        //handlers是一个map,用于存储事件与回调之间的对应关系
        this.handlers = {}
    }
    // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
    on(evnetName, cb){
        // 先检查一下目标事件名有没有对应的监听函数队列
        if(!this.handlers[eventName]){
            // 如果没有,那么首先初始化一个监听函数队列
            this.handlers[evnetName] = []
        }
        // 把回调函数推入目标事件的监听函数队列里去
        this.handlers[eventName].push(cb)
    }
    // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
    emit(eventName,...args){
        // 检查目标事件是否有监听函数队列
        if(this.handlers[eventName]){
            // 这里需要对this.handlers[eventName]做一次浅拷贝,
            主要目的是为了避免通过once安装的监听器在移除的过程中出现顺序问题
            const handlers = this.handlers[eventName].slice()
            // 如果有,则逐个调用队列里的回调函数
            handlers.forEach((callback)=>{
                callback(...args)
            })
        }
    }
    // 移除某个事件回调队列里的指定回调函数
    off(eventName, cb){
        const callbacks = this.handlers[eventName]
        const index = callbacks.indexOf(cb)
        if(index !== -1){
            callbacks.splice(index,1)
        }
    }
    //为事件注册单词监听函数
    once(eventName,cb){
        //对回调函数进行包装,使其执行完毕自动被移除
        const wrapper = (...args)=>{
            cb(...args)
            this.off(eventName, wrapper)
        }
        this.on(eventNme,wrapper)
    }
}