设计模式-观察者模式

121 阅读11分钟

相关概念

观察者模式:直接和公司签合同
订阅者模式:签大厂的外包公司

  • 具体区别
    • 观察者模式:
      • 发布者直接触及到了订阅者
      • 解决的是模块间的耦合问题(只是减少了耦合),可以使得两个分离的、毫不相关的模块也可以实现数据通信
    • 发布订阅模式:
      • 发布者不能直接触及到订阅者,而是由统一的第三方来完成实际的通信操作
      • 发布者完全不用感知订阅者,不用关心怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(事件总线)上,从而实现了完全的解耦

image.png

什么是观察者模式

官方解释

定义对象间的一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

观察者模式又被称为消息机制,它定义了对象间的一种一对多的依赖关系,只要当一个对象的状态发生变化时,所有依赖它的对象都得到通知并自动更新,解决了主体对象与观察者之间功能的耦合,即一个对象与其他对象间的相互通知的问题。

「 是否符合设计原则 」

  • SubjectObserver相互分离,解耦
  • Observer可以自由拓展
  • Subject可自由拓展

观察者模式是由事件发生者在自身状态发生变化时发出通知,由观察者获取消息实现业务逻辑,事件发布者和接受者相互完全解耦,互不影响
在观察者模式中,需要两个主体,分别是目标对象Subject和观察者Observer

  • 观察者需要实现update方法,用于供目标对象使用,该方法中可以执行自定义的业务的代码逻辑;
  • 目标对象需要维护观察者数组数据,并且在自身发生变化时,通过调用自身方法notify,通知到每个观察者执行对应的update方法;
// 定义观察者对象
class Observer {
  /**
   * @param 回调函数 收到目标对象通知时通过update进行执行
   */
  constructor(fn){
    if(typeof fn !== 'function') {
      throw new Error('fn 必须是一个函数类型')
    }else{
      this.fn = fn,
      this.name = fn.name;
    }
  }

  // 目标对象触发更新  执行对应函数进行更新
  update() {
    this.fn(this.name)
  }
}

// 定义目标对象
class Subject {
  constructor(levelList = [1,2,3]) {
    // 维护观察者列表数据
    this.observerList = {
      "normal" : []
    },
    this.levelList = ['normal',...levelList]
  }

  // 添加观察者到对应列表数据中
  addObserver(observer,level="normal"){
    if(!this.levelList.includes(level)) {
      throw new Error(`订阅者 ${observer.name} 的级别错误,请重新设置`)
      return 
    }
    if(!this.observerList[level]) this.observerList[level] = []
    this.observerList[level].push(observer)
  }

  // 删除订阅者
  unloadObserver(observer,level = "normal"){
    if(!this.levelList.includes(level)) {
      throw new Error(`订阅者 ${observer.name} 的级别查询失败,请核实订阅者级别`)
      return 
    }
    if(!this.observerList[level].includes(observer)){
      throw new Error(`订阅者 ${observer.name} 查询失败,请核实订阅者信息是否正确`)
      return 
    }
    this.observerList[level] = this.observerList[level].filter((item) => item !== observer)
  }
  // 通知所有观察者进行更新操作
  notify() {
    this.levelList = this.levelList.sort((a,b) => a-b)
    if(this.levelList.indexOf("normal") === 0 && this.levelList.length > 1){
      this.levelList.push(this.levelList.shift())
    }
    this.levelList.forEach((res) => {
      this.observerList[res].forEach((observer) => {
        observer.update()
      })
    })
  }
}

const Lbxin1 = function(name){
  // ......
  console.log(`成功通知到订阅者 ${name}`)
}
const Lbxin2 = function(name){
  // ......
  console.log(`成功通知到订阅者 ${name}`)
}
const Lbxin3 = function(name){
  // ......
  console.log(`成功通知到订阅者 ${name}`)
}
const Lbxin4 = function(name){
  // ......
  console.log(`成功通知到订阅者 ${name}`)
}
const normal = function(name){
  // ......
  console.log(`成功通知到订阅者 ${name}`)
}
const Lbxin1Ob = new Observer(Lbxin1)
const Lbxin2Ob = new Observer(Lbxin2)
const Lbxin3Ob = new Observer(Lbxin3)
const Lbxin4Ob = new Observer(Lbxin4)
const normalOb = new Observer(normal)

const subject = new Subject([1,3,2])
subject.addObserver(Lbxin1Ob,2)
subject.addObserver(Lbxin2Ob,3)
subject.addObserver(Lbxin3Ob,1)
subject.addObserver(Lbxin4Ob,2)
subject.addObserver(normalOb,1)
subject.addObserver(normalOb,2)
subject.addObserver(normalOb)
subject.addObserver(normalOb)
// subject.addObserver(Lbxin4Ob,4)
// sodu Error: 订阅者 Lbxin4 的级别错误,请重新设置

// subject.unloadObserver(normalOb,5)
// sodu Error: 订阅者 normal 的级别查询失败,请核实订阅者级别

// const normalOb11 = new Observer(normal)
// subject.unloadObserver(normalOb11)
// sodu Error: 订阅者 normal 查询失败,请核实订阅者信息是否正确

subject.unloadObserver(normalOb,1)

subject.notify()

// 成功通知到订阅者 Lbxin3
// 成功通知到订阅者 Lbxin1
// 成功通知到订阅者 Lbxin4
// 成功通知到订阅者 normal
// 成功通知到订阅者 Lbxin2
// 成功通知到订阅者 normal
// 成功通知到订阅者 normal

次例与下方的实现优点在于将观察者和目标对象分离,角色各自目的很明确,没有像发布订阅者模式中的调度中心作为中间者,实现了相关的抽离和拓展;

观察者模式的相关应用

postMessage 通信

利用window对象的PostMessageAPI进行跨页面组件的通信,例如iframe间的相关通信,多进程(nodjs weborker)通讯,websocket通讯等

image.png image.png

events模块的使用

node中的events是一个使用率很高的模块,其他的原生node模块都是基于它完成的,比如流、HTTP等;其底层还是用了观察者模式;
events模块的功能就是一个事件绑定,所有继承它的实例都具备事件处理的能力;

Vue框架的应用

生命周期、watch、组件更新过程,监听DOM树变化的MutationObserver,各种异步回调包括:定时器、Promise.then、node的stream readline httpServer等;

MutationObserver.png node的stream.png

适用场景
  • 在一个抽象模型中,一个对象的行为依赖于另一个对象的状态,即一个对象的状态变化会影响另一个对象的行为数据
  • 在系统中创建一个触发链,A影响B,B影响C......
数据的双向绑定

利用 Object.defineProperty() 对数据进行劫持,设置一个监听器 Observer,用来监听所有属性,如果属性发上变化了,就告诉订阅者 Watcher 去更新数据,最后指令解析器 Compile 解析对应的指令,进而会执行对应的更新函数,从而更新视图,实现了数据与视图的双向绑定

image.png

父子组件的通信

Vue 中我们通过 props 完成父组件向子组件传递数据,子组件与父组件通信我们通过自定义事件即 $on,$emit来实现,其实也就是通过 $emit 来发布消息,并对订阅者 $on 做统一处理;
在非父子组件通信中,可以用中央事件总线进行相关通信,此时也类似与发布订阅模式

DOM的事件监听
document.querySelector('#btn').addEventListener('click',function () {
    console.log('按钮被点击了');
},false)
解决了一堆元素与相同数据同步的长期问题

codepen-观察者模式案例

拓展

  1. DOM方法 addEventListener() 和 removeEventListener()是用来分配和删除事件的函数。 这两个方法都需要三个参数,分别为:
    • 事件名称(String)、要触发的事件处理函数(Function)、指定事件处理函数的时期或阶段(boolean)
    • 当第三个参数设置为true就在捕获过程中执行,反之就在冒泡过程中执行处理函数 image.png
  2. 什么是事件流
    "DOM2事件流"规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段;
  3. document,documentElement和document.body三者之间的关系
    • document代表的是整个html页面
    • document.documentElement代表是的<html>标签
    • document.body代表的是<body>标签
  4. 事件穿透
    • 当事件为冒泡时,从DOM树的最下面往上一层层执行事件(界面表现上为最顶层盒子事件先执行,以此类推);
    • 当事件为捕获时,从DOM树的最上面一层层执行事件(界面表现为最底层盒子事件先执行,以此类推);

观察者模式优缺点

优点
  • 可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并且降低观察目标和观察者之间的耦合度
  • 支持简单广播通信自动通知到所有已订阅过的对象
  • 符合开放封闭要求(对拓展开放,对修改封闭)
    • 观察目标可以对应多个观察者,这些观察者之间没有相互联系,可以通过增加和删除观察者,来使得系统更易于拓展;
  • 观察目标和观察者之间的抽象耦合关系能够单独拓展和重用
缺点
  • 通知的时间成本,当有多个观察者时
  • 当观察目标和观察者之间存在循环引用时,有可能会出现系统崩溃的情况
  • 观察目标和观察者之间是不透明的,无法知道两者的具体逻辑

具体案例分析

Vue中观察者模式的相关应用
  • Event Bus和Event Emitter - 发布订阅者模式
  • 数据双向绑定也是应用了观察者模式
    • 主要有三个角色
      • observer(监听器):常规的发布订阅者中的observer代表订阅者,但在Vue双向绑定中observer不仅仅是数据监听器,还负责数据的转发(即也是一个发布者)
      • watcher(订阅者):Observer将数据转发给了真正的订阅者 watcher对象,watcher接收到新的数据后会去更新视图
      • compile(编译器):MVVM框架特有的角色,负责每个节点元素指令进行扫描和解析,指令的初始化、订阅者的创建等杂活都进行统一管理 image.png
发布订阅继承式实现「需求开发」
  • 基类定义
// 定义发布者类
// 主要是添加、移除和通知的功能
class Publisher {
  constructor() {
    this.observers = []
    console.log('Publisher created')
  }
  // 增加订阅者
  add(observer) {
    console.log('Publisher.add invoked')
    this.observers.push(observer)
  }
  // 移除订阅者
  remove(observer) {
    console.log('Publisher.remove invoked')
    this.observers.forEach((item, i) => {
      if (item === observer) {
        this.observers.splice(i, 1)
      }
    })
  }
  // 通知所有订阅者
  notify() {
    console.log('Publisher.notify invoked')
    this.observers.forEach((observer) => {
      observer.update(this)
    })
  }
}
// 定义订阅者类
// 被动方(被通知和后续'被迫'执行),主要是更新逻辑 具体触发机制是在发布者类中的「通知」模块中进行的
class Observer {
    constructor() {
        console.log('Observer created')
    }

    update() {
        console.log('Observer.update invoked')
    }
}
  • 基于基类定制化开发
// 发布方定制化 根据某一个状态来使得「订阅者监听某个特定状态的变化」
// 定义一个具体的需求文档(prd)发布类
class PrdPublisher extends Publisher {
    constructor() {
        super()
        // 初始化需求文档
        this.prdState = null
        // 韩梅梅还没有拉群,开发群目前为空
        // ? this.observers = []
        console.log('PrdPublisher created')
    }
    
    // 该方法用于获取当前的prdState
    getState() {
        console.log('PrdPublisher.getState invoked')
        return this.prdState
    }
    
    // 该方法用于改变prdState的值
    setState(state) {
        console.log('PrdPublisher.setState invoked')
        // prd的值发生改变
        this.prdState = state
        // 需求文档变更,立刻通知所有开发者
        this.notify()
    }
}
// 订阅方定制化  使得开发者任务变得具体起来
class DeveloperObserver extends Observer {
    constructor() {
        super()
        // 需求文档一开始还不存在,prd初始为空对象
        this.prdState = {}
        console.log('DeveloperObserver created')
    }
    
    // 重写一个具体的update方法
    update(publisher) {
        console.log('DeveloperObserver.update invoked')
        // 更新需求文档
        this.prdState = publisher.getState()
        // 调用工作函数
        this.work()
    }
    
    // work方法,一个专门搬砖的方法
    work() {
        // 获取需求文档
        const prd = this.prdState
        // 开始基于需求文档提供的信息搬砖。。。
        ...
        console.log('996 begins...')
    }
}

通过new一个PrdPublisher对象来通过setState来通知订阅者进行触发执行指定的逻辑,从而实现目标对象的状态变化时,会通知所有观察者对象,使得它们可以自动更新

// 创建订阅者:前端开发李雷
const liLei = new DeveloperObserver()
// 创建订阅者:服务端开发小A
const A = new DeveloperObserver()
// 创建订阅者:测试同学小B
const B = new DeveloperObserver()
// 韩梅梅出现了
const hanMeiMei = new PrdPublisher()
// 需求文档出现了
const prd = {
    // 具体的需求内容
    ...
}
// 韩梅梅开始拉群
hanMeiMei.add(liLei)
hanMeiMei.add(A)
hanMeiMei.add(B)
// 韩梅梅发送了需求文档,并@了所有人
hanMeiMei.setState(prd)
订阅报刊

在创建报刊的过程中会有很多的额外的操作,并且在业务发展过程中,会不断的添加或修改这些额外的逻辑,所以适合用观察者模式进行设计;
发布者Magazine需要有一个subscribers属性,是一个数组类型,后续的订阅行为都通过指定方法存放到这个数组中,当有新报刊时需要通知操作,该操作即是调用订阅者存放的这些方法;

观察者模式需要有的API包括

  • subscribers属性用于存放订阅者的行为逻辑 subscribers
  • 添加订阅,保存订阅逻辑到subscribers中 subscribe()
  • 移除订阅的API,用于将订阅逻辑从subscribers中删除 unsubscribe()
  • 通知订阅者的API,用于调用订阅者的订阅方法 publish()
  • 区分订阅类型的变量值,用于区分订阅的报刊类型 type
const Magazine = {
  // 存放订阅者方法
  subscribers: {
    'any': []
  },
  // 订阅
  subscribe: function(type = 'any',fn){
    if(!this.subscribers[type]){
      this.subscribers[type] = []
    }
    this.subscribers[type].push(fn)
  },
  //退订
  unsubscribe:function(type = 'any', fn){
    this.subscribers[type] = this.subscribers[type].filter((item)=>item !== fn)
  },
  // 发布更新的消息
  publish(type='any',...args){
    // 根据不同类型进行指定类型的订阅者通知
    this.subscribers[type].forEach((item) => {
      item(...args)
    })
  }
}

function subscribeBack(info){
  console.log("订阅的报刊更新啦:",info)
}
const subscribeList = [
  {
    name: 'Lbxin01',
    type: 'front-end',
    address: '上地'
  },
  {
    name: 'Lbxin02',
    type: 'rear-end',
    address: '十里堡'
  }
]
subscribeList.forEach(({name,type,address}) => {
  Magazine.subscribe(type,subscribeBack)
})

Magazine.publish('front-end','vue3.0 正式发布')
Magazine.publish('rear-end','deno 更新中')
Magazine.unsubscribe('rear-end',subscribeBack)
Magazine.publish('front-end','TS 8.0正式发布')
Magazine.publish('rear-end','node 升级迭代上线了')

// 订阅的报刊更新啦: vue3.0 正式发布
// 订阅的报刊更新啦: deno 更新中
// 订阅的报刊更新啦: TS 8.0正式发布