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

生活中的观察者模式

比如当我们进入一个聊天室 / 群,如果有人在聊天室发言,那么这个聊天室里的所有人都会收到这个人的发言。这是一个典型的发布 - 订阅模式,当我们加入了这个群,相当于订阅了在这个聊天室发送的消息,当有新的消息产生,聊天室会负责将消息发布给所有聊天室的订阅者。
角色划分 —> 状态变化 —> 发布者通知到订阅者,这就是观察者模式的“套路”。

定义

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象

总述:

主要有下面几个概念:

  1. Publisher :发布者
  2. Subscriber :订阅者
  3. SubscriberMap :持有不同 type 的数组,存储有所有订阅者的数组
  4. type :消息类型,订阅者可以订阅的不同消息类型
  5. subscribe :添加订阅者
  6. unSubscribe :删除订阅者
  7. notify :该方法遍历通知 SubscriberMap 中对应 type 的每个订阅者

结构如下图:
观察者模式 - 图2


发布者类

基本技能包括:

  • 是拉群(增加订阅者),
  • @所有人(通知订阅者),
  • 踢走项目组成员(移除订阅者)

    1. // 定义发布者类
    2. class Publisher {
    3. constructor() {
    4. this.observers = [] // 订阅者队列
    5. console.log('Publisher created')
    6. }
    7. // 增加订阅者
    8. add(observer) {
    9. console.log('Publisher.add invoked')
    10. this.observers.push(observer)
    11. }
    12. // 移除订阅者
    13. remove(observer) {
    14. console.log('Publisher.remove invoked')
    15. this.observers.forEach((item, i) => {
    16. if (item === observer) {
    17. this.observers.splice(i, 1)
    18. }
    19. })
    20. }
    21. // 通知所有订阅者
    22. notify() {
    23. console.log('Publisher.notify invoked')
    24. this.observers.forEach((observer) => {
    25. observer.update(this)
    26. })
    27. }
    28. }

    订阅者

  • 被通知
  • 去执行(本质上是接受发布者的调用,这步我们在Publisher中已经做掉了)。
    1. // 定义订阅者类
    2. class Observer {
    3. constructor() {
    4. console.log('Observer created')
    5. }
    6. update() {
    7. console.log('Observer.update invoked')
    8. }
    9. }

    实践

    基于基类来定制自己的发布者/订阅者逻辑。如:让订阅者们来监听需求文档(prd)的变化:

发布者:

  1. // 定义一个具体的需求文档(prd)发布类
  2. class PrdPublisher extends Publisher {
  3. constructor() {
  4. super()
  5. // 初始化需求文档
  6. this.prdState = null
  7. // 韩梅梅还没有拉群,开发群目前为空
  8. this.observers = []
  9. console.log('PrdPublisher created')
  10. }
  11. // 该方法用于获取当前的prdState
  12. getState() {
  13. console.log('PrdPublisher.getState invoked')
  14. return this.prdState
  15. }
  16. // 该方法用于改变prdState的值
  17. setState(state) {
  18. console.log('PrdPublisher.setState invoked')
  19. // prd的值发生改变
  20. this.prdState = state
  21. // 需求文档变更,立刻通知所有开发者
  22. this.notify()
  23. }
  24. }

订阅方:

接收需求文档、并开始干活

  1. class DeveloperObserver extends Observer {
  2. constructor() {
  3. super()
  4. // 需求文档一开始还不存在,prd初始为空对象
  5. this.prdState = {}
  6. console.log('DeveloperObserver created')
  7. }
  8. // 重写一个具体的update方法
  9. update(publisher) {
  10. console.log('DeveloperObserver.update invoked')
  11. // 更新需求文档
  12. this.prdState = publisher.getState()
  13. // 调用工作函数
  14. this.work()
  15. }
  16. // work方法,一个专门搬砖的方法
  17. work() {
  18. // 获取需求文档
  19. const prd = this.prdState
  20. // 开始基于需求文档提供的信息搬砖。。。
  21. ...
  22. console.log('996 begins...')
  23. }
  24. }

工作:

  • PrdPublisher 对象,可以通过调用 setState 方法来更新需求文档。
  • 需求文档每次更新,都会紧接着调用 notify 方法来通知所有开发者,当然也可特定的逻辑通知到个别订阅**者**:
    1. 目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
  1. // 创建订阅者:前端开发李雷
  2. const liLei = new DeveloperObserver()
  3. // 创建订阅者:服务端开发小A(sorry。。。起名字真的太难了)
  4. const A = new DeveloperObserver()
  5. // 发布者:韩梅梅出现了
  6. const hanMeiMei = new PrdPublisher()
  7. // 需求文档出现了
  8. const prd = {
  9. // 具体的需求内容
  10. ...
  11. }
  12. // 韩梅梅开始拉群
  13. hanMeiMei.add(liLei)
  14. hanMeiMei.add(A)
  15. // 韩梅梅发送了需求文档,并@了所有人
  16. hanMeiMei.setState(prd)

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

解析


过程分析

观察者模式 - 图3
在 Vue 中,组件渲染函数(Render Function)被执行前,会对数据层的数据进行响应式化,即:

这个列表是 getter 闭包中的属性,将会记录所有依赖这个数据的组件
响应式化后的数据相当于发布者

  • 每个 组件实例 都有相应的 watcher 实例对象,它会在组件渲染的过程中将本组件的 Watcher 放到自己所依赖的响应式数据的订阅者列表里,这就相当于完成了订阅,一般这个过程被称为依赖收集。

**

  • 当响应式数据发生变化的时候,会触发 settersetter 会负责通知(Notify)该数据的订阅者列表里的 Watcher,Watcher 会触发组件**重新渲染(Trigger re-render)来更新(update)视图**。

这是一个典型的观察者模式。

在Vue数据双向绑定的实现逻辑里,有这样三个关键角色:

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

这三者的配合过程如图所示:
观察者模式 - 图4

源码分析

  1. Object.defineProperty(obj, key, {
  2. enumerable: true,
  3. configurable: true,
  4. get: function reactiveGetter() {
  5. ...
  6. // 如果原本对象拥有getter方法则执行
  7. const value = getter ? getter.call(obj) : val
  8. dep.depend() // 进行依赖收集,dep.addSub
  9. return value
  10. },
  11. set: function reactiveSetter(newVal) {
  12. ...
  13. // 如果原本对象拥有setter方法则执行
  14. if (setter) { setter.call(obj, newVal) }
  15. dep.notify() // 如果发生变更,则通知更新
  16. }
  17. })

而这个 dep 上的 dependnotify 就是订阅和发布通知的具体方法。


核心代码

实现observer

  • 对需要监听的数据对象进行遍历
  • 给它的属性加上定制的 gettersetter 函数。
    • 当这个对象的某个属性发生了改变,就会触发 setter 函数,进而通知到订阅者。

这个 setter 函数,就是我们的监听器

  1. // observe方法遍历并包装对象属性
  2. function observe(target) {
  3. // 若target是一个对象,则遍历它
  4. if(target && typeof target === 'object') {
  5. Object.keys(target).forEach((key)=> {
  6. // defineReactive方法会给目标属性装上“监听器”
  7. defineReactive(target, key, target[key])
  8. })
  9. }
  10. }
  11. // 定义defineReactive方法
  12. function defineReactive(target, key, val) {
  13. // 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历
  14. observe(val)
  15. // 为当前属性安装监听器
  16. Object.defineProperty(target, key, {
  17. // 可枚举
  18. enumerable: true,
  19. // 不可配置
  20. configurable: false,
  21. get: function () {
  22. return val;
  23. },
  24. // 监听器函数
  25. set: function (value) {
  26. console.log(`${target}属性的${key}属性从${val}值变成了了${value}`)
  27. val = value
  28. }
  29. });
  30. }


实现订阅者 Dep

  1. // 定义订阅者类Dep
  2. class Dep {
  3. constructor() {
  4. // 初始化订阅队列
  5. this.subs = []
  6. }
  7. // 增加订阅者
  8. addSub(sub) {
  9. this.subs.push(sub)
  10. }
  11. // 通知订阅者
  12. notify() {
  13. this.subs.forEach((sub)=>{
  14. sub.update()
  15. })
  16. }
  17. }


改写 defineReactive

改写setter 方法,在监听器里去通知订阅者

  1. function defineReactive(target, key, val) {
  2. const dep = new Dep()
  3. // 监听当前属性
  4. observe(val)
  5. Object.defineProperty(target, key, {
  6. set: (value) => {
  7. // 通知所有订阅者
  8. dep.notify()
  9. }
  10. })
  11. }


Event Bus/ Event Emitter

Event Bus/Event Emitter 作为全局事件总线,它起到的是一个沟通桥梁的作用
所有事件的订阅/发布都不能由订阅方和发布方“私下沟通”,必须要委托这个事件中心帮我们实现。

  1. class EventEmitter {
  2. constructor() {
  3. // handlers是一个map,用于存储事件与回调之间的对应关系
  4. this.handlers = {}
  5. }
  6. // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
  7. on(eventName, cb) {
  8. // 先检查一下目标事件名有没有对应的监听函数队列
  9. if (!this.handlers[eventName]) {
  10. // 如果没有,那么首先初始化一个监听函数队列
  11. this.handlers[eventName] = []
  12. }
  13. // 把回调函数推入目标事件的监听函数队列里去
  14. this.handlers[eventName].push(cb)
  15. }
  16. // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
  17. emit(eventName, ...args) {
  18. // 检查目标事件是否有监听函数队列
  19. if (this.handlers[eventName]) {
  20. // 如果有,则逐个调用队列里的回调函数
  21. this.handlers[eventName].forEach((callback) => {
  22. callback(...args)
  23. })
  24. }
  25. }
  26. // 移除某个事件回调队列里的指定回调函数
  27. off(eventName, cb) {
  28. const callbacks = this.handlers[eventName]
  29. const index = callbacks.indexOf(cb)
  30. if (index !== -1) {
  31. callbacks.splice(index, 1)
  32. }
  33. }
  34. // 为事件注册单次监听器
  35. once(eventName, cb) {
  36. // 对回调函数进行包装,使其执行完毕自动被移除
  37. const wrapper = (...args) => {
  38. cb(...args)
  39. this.off(eventName, wrapper)
  40. }
  41. this.on(eventName, wrapper)
  42. }
  43. }

在React中使用Event Bus来实现组件间的通讯

观察者模式与发布-订阅模式的区别是什么?

观察者模式:

发布者直接触及到订阅者的操作:

  • 韩梅梅把所有的开发者拉了一个群,直接把需求文档丢给每一位群成员

    观察者模式 - 图5

    解决的

发布-订阅模式:

发布者不直接触及到订阅者,而是由统一的第三方来完成实际的通信的操作:

  • 韩梅梅没有拉群,而是把需求文档上传到了公司统一的需求平台上,需求平台感知到文件的变化、自动通知了每一位订阅了该文件的开发者
  • 通过EventBus去实现事件监听/发布

观察者模式 - 图6

解决的问题:

两者都解决了模块间的耦合问题

  • 两个分离的、毫不相关的模块,也可以实现数据通信。
  • 监听事件的位置和触发事件的位置是不受限的,只要它们在同一个上下文里,就能够彼此感知。

观察者模式:

  • 仅仅是减少了耦合并没有完全地解决耦合问题

    • 被观察者必须去维护一套观察者的集合,这些观察者必须实现统一的方法供被观察者调用


      发布-订阅模式
  • 实现了完全地解耦