观察者模式

144 阅读4分钟

场景

用户点了网页中的按钮,接下要做三件事情(假设三件事没有依赖关系)。

初始方案1:伪代码:

document.getElementById('btn').addEventListener('click', {
  doSomething1()
  doSomething2()
  doSomething3()
})

上面的三个函数表示三件不同的事情。

好了,就从这里出发,我们看着这段代码,提一个需求:额外再补充做一件事。

解决:定义一个函数doSomething4,然后添加到click的回调中,如下:

document.getElementById('btn').addEventListener('click', {
  doSomething1()
  doSomething2()
  doSomething3()
  doSomething4() // 这是新添加的
})

看起来,需求可以很方便的满足,也没有什么问题。但是,我们直接修改了 原来的初始代码 。

大家会觉得:添加新功能要改原来的代码,不是很正常吗,这些代码是不都是我们自己写的吗,改改有什么关系?

有关系,两个理由:

  • 理论上:违反了软件设计的开闭原则
  • 实操中:原来的代码是加密的,或者看起来就非常复杂,或者你在远程指导其他人来完成这个需求,或者不是我们自己写的..... 这些情况都让 直接修改原来代码 变得很困难。

以上是背景,下面引出观察者模式的写法。来!

改进方案:伪代码

document.getElementById('btn').addEventListener('click', doSomething1)
document.getElementById('btn').addEventListener('click', doSomething2)
document.getElementById('btn').addEventListener('click', doSomething3)

需求:额外再补充做一件事。不需要改动原来的代码哈,只需要添加一句:


document.getElementById('btn').addEventListener('click', doSomething1)
document.getElementById('btn').addEventListener('click', doSomething2)
document.getElementById('btn').addEventListener('click', doSomething3)
// 额外再补充做一件事
document.getElementById('btn').addEventListener('click', doSomething4) 

现在有没有觉得,改进的方案比初始方案好呢?其实,改进的方案中就使用了观察者模式,这是DOM2级事件机制提供给我们的便利。

观察者模式的定义

观察者模式是23种设计模式中的一种。 设计模式就是解决一类编码问题的最优解,就是套路。这个概念与具体的编程语言关系不大,但是由于各个编程语言的特性不同,同一个设计模式在不同的语言中的实现成本也不同。

观察者模式的英文是Observer,下面来看它的两个经典定义:

《js设计模式》中对Observer的定义:一个对象(称为subject)维持一系列依赖于它(观察者)的对象,将有关状态的任何变更自动通知给它们。

《设计模式:可复用面向对象软件的基础》中对Observer的定义:一个或多个观察者对目标的状态感兴趣,他们通过将自己依附在目标对象上以便注册所感兴趣的内容。目标状态发生改变并且观察者可能对这些改变感兴趣,就会发送一个通知消息,调用每个观察者的更新方法。当观察者不再对目标感兴趣时,他们可以简单地将自己从中分离。

注意一下,在各个不同的资料(语言背景)中可能看到的定义表述是不同的,不过,他们有共同点:

  • 描述一对多的关系。多个观察者对一个目标感兴趣
  • 目标变化之后,会主动通知观察者(他们分别做出各自的行为)
  • 观察者是可以任意添加和移除的(对目标不再感兴趣就移除)

我们再来看看前面写的代码,检查是否符合上面的标准:

document.getElementById('bnt').addEventListener('click', doSomething1)
document.getElementById('bnt').addEventListener('click', doSomething2)
document.getElementById('bnt').addEventListener('click', doSomething3)
document.getElementById('bnt').addEventListener('click', doSomething4) 
// 额外再补充做一件事

说明如下:

  • 描述一对多的关系。四个观察者(doSomething1,2,3, 4)对一个目标(按钮是否点击)感兴趣
  • 目标变化之后,会主动通知观察者(按钮点击之后,4个动作各自执行)
  • 观察者是可以任意添加和删除(addEventListener来添加观察者,removeEventListener来删除观察者)

写一个观察者模式

下面,用一段简单的代码来实现观察者模式:

// 大漂亮是女神,有很多的爱慕者
const beauty = {
    lover: [], // 容器,保存全部的观察者
    notify(){ // 通知全部观察者
      this.lover.forEach(fn=>fn())
    },
    addLover(fn) { // 添加观察者
      this.lover.push(fn)
    },
    removeLover(fn) { // 移除观察者
      const idx = this.lover.findIndex(item=>item ===fn);
      if(idx !=-1) {
        this.lover.splice(idx,1)
      }
    }
  }
  const lover1 = () => {console.log('1号爱慕者')}
  const lover2 = () => {console.log('2号爱慕者')}
  const lover3 = () => {console.log('3号爱慕者')}
  beauty.addLover(lover1) // 添加观察者
  beauty.addLover(lover2) // 添加观察者
  beauty.addLover(lover3) // 添加观察者
  beauty.notify()// 1,2,3号
  beauty.removeLover(lover1) // 移除观察者
  beauty.notify()// 2,3号

上面代码的核心要点:

  • 一个容器来保存观察者
  • 一个动作通知全体观察者
  • 两个动作(添加,移除)来操作观察者

简单应用

看个观察者模式的应用。

下边的代码实现了一个效果:修改了对象的属性值,在视图上有两个地方变化了。 mmbiz.qpic.cn/mmbiz_gif/g…

一个值变了,两个地方跟着做出改变。是不是一个使用观察者的好场景呢?以下是实现代码

<body>
  <div id="id1"></div>
  <div id="id2"></div>
</body>
<script>
  function observer(key,_value){
    let obj = {}
    let listers = []
    function notify() {listers.forEach(fn => fn(_value))}
    function addLister (fn) {listers.push(fn)}
    Object.defineProperty(obj,key, {
      get() {
        return _value
      },
      set(newVal) {
        if(newVal !== _value) {
          _value = newVal
          notify()
        }
      }
    })
    return  { obj, addLister}
  }
  const {obj, addLister} = observer('age', 10)
  addLister(val => {document.getElementById('id1').innerHTML = 'listener1  ' + val})
  addLister(val => {document.getElementById('id2').innerHTML = 'listener2  ' + 2*val})
  obj.age = 10
</script>

小结

本文介绍了观察者模式用来解决的问题、定义,基本实现,最后完成了一个小小的响应式 功能,如果对你理解观察者模式有帮助,欢迎你转发,关注,小额打赏。

搬运自:观察者模式