# 观察者模式初相识

观察者模式

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

观察者模式有一个"别名"叫 - 发布-订阅模式,这个别名是加引号的,这两者之间还是有区别的,但是从这个别名我们可以得到个关键的信息 - 观察者模式需要有发布者和订阅者两个核心角色组成。

上面都是一些理论知识,对于一些没有基础的同学可能理解起来比较困难。下面我们来举一个生活中的例子,希望能帮大家更好地理解。

比如今天是你入职的第一天,你的直属领导会把你拉到工作群里(如果你试用期没过,他也会把你移出工作群)方便以后的工作。过了一会儿,你的领导在群里艾特了所有人,通知大家10:30开会。因为现在还没有到时间,大家先不用去开会。但是呢,因为接受到了这个通知,所以大家时不时的都要看一下表,等到10:30一到,一起去会议室开会,但是你的直属领导可能在10:30有其他紧急的事情要处理,这个时候到,会把开会时间改为其他时段,并在群里通知大家,这个过程就是一个典型的观察者模式。

在上面这个例子中,你的直属领导就是发布者,在工作群里的其他同事就是订阅者,也就是观察者对象。简单总结下。观察者模式的套路:角色划分 -> 状态变化 -> 发布者通知订阅者。

从上面的例子我们可以看出,发布者有三个最基本的功能分别是:增加订阅者,删除订阅者和通知订阅者。

// 定义发布者类
class Publisher {
 constructor() {
   this.observers = []
 }

 add(observer) {
   this.observers.push(observer)
 }

 remove(observer) {
   this.observers = this.observers.filter(item => item !== observer)
 }

 notify() {
   this.observers.forEach(observer => observer.update(this))
 }
}

订阅者有两个基本的职能分别是被通知和去执行(接受发布者的调用)。

class Observer {
    constructor() {
        console.log('Observer created')
    }

    update() {
        console.log('Observer.update invoked')
    }
}

上面我们就完成了最基本的发布者和订阅者类的设计和编写,但是呢,还缺少一个非常重要的环节 - 作为发布者,你到底发布了啥?作为订阅者,我又订阅了啥?下面我们来完善一下。

// 定义一个具体的需求文档(prd)发布类
class MeetingPublisher extends Publisher {
    constructor() {
        super()
        // 初始化会议时间
        this.meetingTime = null
        // 直属领导还没拉群,群目前为空
        this.observers = []
        console.log('MeetingPublisher created')
    }
    
    // 该方法用于获取当前的prdState
    getState() {
        console.log('MeetingPublisher.getState invoked')
        return this.meetingTime
    }
    
    // 该方法用于改变 mettingTime 的值
    setState(state) {
        console.log('MeetingPublisher.setState invoked')
        // 会议时间的值发生改变
        this.mettingTime = state
        // 会议时间变更,立刻通知所有成员
        this.notify()
    }
}

参会人员接受开会时间,并去开会。

class AttendeesObserver extends Observer {
    constructor() {
        super()
        // 会议时间还不存在,初始为空字符串
        this.meetingTime = ''
        console.log('DeveloperObserver created')
    }
    
    // 重写一个具体的update方法
    update(publisher) {
        console.log('DeveloperObserver.update invoked')
        // 更新开会时间
        this.meetingTime = publisher.getState()
        // 调用开会函数
        this.metting()
    }
    
    // metting方法,一个开会的方法
    metting() {
        // 获取开会时间
        const meetingTime = this.meetingTime
        // 开始开会。。。
        ...
        console.log('bala bala bala...')
    }
}

上面我们也就实现了,目标对象(开会时间)的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。下面我们来看一下它的整体流程:

// 创建与会者 a、b、c
const a = new AttendeesObserver();
const b = new AttendeesObserver();
const c = new AttendeesObserver();
// 创建会议发起者
const manager = new MeetingPublisher();
// 开会时间
const meetingTime = '10:30'

// 拉群
manager.add(a);
manager.add(b);
manager.add(c);

// 设置会议时间
manager.setState(meetingTime);

上面就是对观察者模式的一个简要概述,希望对大家有帮助。

# 应用实践

下面我们来看一下观察者模式的真实应用场景。

# Vue数据双向绑定(响应式系统)的实现原理

关于Vue的响应式原理可以看下官方文档深入响应式原理 (opens new window),这里仅做简要说明。

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

Vue数据双向绑定的实现逻辑里有三个重要角色:

  • observer(监听器):注意,此 observer 非彼 observer。在我们上节的解析中,observer 作为设计模式中的一个角色,代表“订阅者”。但在Vue数据双向绑定的角色结构里,所谓的 observer 不仅是一个数据监听器,它还需要对监听到的数据进行转发——也就是说它同时还是一个发布者。
  • watcher(订阅者):observer 把数据转发给了真正的订阅者——watcher对象。watcher 接收到新的数据后,会去更新视图。
  • compile(编译器):MVVM 框架特有的角色,负责对每个节点元素指令进行扫描和解析,指令的数据初始化、订阅者的创建这些“杂活”也归它管

observer 的实现:

// 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 (value) {
            console.log(`${target}属性的${key}属性从${val}值变成了了${value}`)
            val = value
        }
    });
}

订阅者 Dep 的实现:

// 定义订阅者类Dep
class Dep {
    constructor() {
        // 初始化订阅队列
        this.subs = []
    }
    
    // 增加订阅者
    addSub(sub) {
        this.subs.push(sub)
    }
    
    // 通知订阅者(是不是所有的代码都似曾相识?)
    notify() {
        this.subs.forEach((sub)=>{
            sub.update()
        })
    }
}

在监听器中通知订阅者:







 
 




function defineReactive(target, key, val) {
    const dep = new Dep()
    // 监听当前属性
    observe(val)
    Object.defineProperty(target, key, {
        set: (value) => {
            // 通知所有订阅者
            dep.notify()
        }
    })
}

# Event Bus

全局事件总线严格来说不是观察者模式,而是发布-订阅模式。

我们来简单看下 Event Bus 在 Vue 中的使用:

const EventBus = new Vue()
export default EventBus
import bus from 'EventBus的文件路径'
Vue.prototype.bus = bus

订阅事件:

// 这里func指someEvent这个事件的监听函数
this.bus.$on('someEvent', func)

发布事件:

// 这里params指someEvent这个事件被触发时回调函数接收的入参
this.bus.$emit('someEvent', params)
class EventEmitter {
  constructor() {
    // handlers是一个map,用于存储事件与回调之间的对应关系
    this.handlers = {}
  }

  // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
  on(eventName, cb) {
    // 先检查一下目标事件名有没有对应的监听函数队列
    if (!this.handlers[eventName]) {
      // 如果没有,那么首先初始化一个监听函数队列
      this.handlers[eventName] = []
    }

    // 把回调函数推入目标事件的监听函数队列里去
    this.handlers[eventName].push(cb)
  }

  // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
  emit(eventName, ...args) {
    // 检查目标事件是否有监听函数队列
    if (this.handlers[eventName]) {
      // 如果有,则逐个调用队列里的回调函数
      this.handlers[eventName].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(eventName, wrapper)
  }
}

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

发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式。观察者模式和发布-订阅模式之间的区别,在于是否存在第三方、发布者能否直接感知订阅者。

观察者模式是为了减少模块间的耦合,但是并没有完全解决耦合问题。发布-订阅模式,事件的注册和触发独立于第三方平台上,实现了完全解耦。

Vue 在哪个生命周期发送请求 Vue组件生命周期执行顺序 Vue组件通信 v-if和v-for为什么不建议一起使用 虚拟dom的优缺点 vue中key的作用 keep alive的使用场景和原理