前端最重要的设计模式: 观察者模式

853 阅读4分钟

什么是观察者模式

你在星巴克点了咖啡,此时你并不需要在吧台坐等,你只需要回到位子上玩手机,等咖啡好了服务员会叫你。这就是观察者模式。

DOM 事件就是最常用的观察者模式

<button id="btn1">btn</button>

<script>
    const $btn1 = $('#btn1')
    $btn1.click(function () {
        console.log(1)
    })
    $btn1.click(function () {
        console.log(2)
    })
</script>

我不知道用户什么时候点击,所以我先把事件都注册上,等你点击之后就执行我注册的事件。

除了DOM事件外,Vue React 的生命周期,也是观察者模式

代码实现

Subject可以看出是星巴克,Observer就是买咖啡的人。

// 主题
class Subject {
  private state: number = 0
  private observers: Observer[] = []

  getState(): number {
    return this.state
  }

  setState(newState: number) {
    this.state = newState
    this.notify()
  }

  attach(observer: Observer) {
    this.observers.push(observer)
  }

  private notify() {
    for (let observer of this.observers) {
      observer.update(this.state)
    }
  }
}

// 观察者
class Observer {
  name: string
  constructor(name: string) {
    this.name = name
  }

  update(state: number) {
    console.log(`${this.name} update, state is ${state}`)
  }
}

const sub = new Subject()
const observer1 = new Observer('A')
sub.attach(observer1)
const observer2 = new Observer('B')
sub.attach(observer2)

sub.setState(1) // 更新状态,触发观察者 update

image.png

从图中可以看到主题和观察者是1:n的形式,1:n说明它的扩展性非常好,它可以是1:2,也可以是1:3。

观察者模式应用场景

Vue watch

{
    data() {
        name: '双越'
    },
    watch: {
        name(newVal, val) {
            console.log(newValue, val)
        }
    }
}

data就是主题,watch里面的函数就是观察者,当主题data发生变化的时候,观察者就执行对应的操作。

Vue 组件更新过程

image.png

Watcher就是观察者,data就是主题,当data变化的时候通过notify通知watcher重新渲染re-render。

Vue 组件生命周期

组件的生命周期也是观察者模式,我们在各个生命周期钩子里面绑定需要执行的逻辑,等到组件执行到对应的时机就会去执行绑定的逻辑。

当你开发自己的 lib 时,也要考虑它的完整生命周期,从开始的创建,到销毁环节。

各种异步的回调

常用的定时器 setTimeout setInterval,以及 Promise.then 的回调函数,也是属于观察者模式,跟DOM事件绑定类似。

nodejs经常需要进行IO操作,回调函数也是属于观察者模式。

nodejs stream

const fs = require('fs')
const readStream = fs.createReadStream('./data/file1.txt') 
readStream.on('data', function (chunk) {})
readStream.on('end', function () {})

nodejs readline

const readline = require('readline');
const fs = require('fs')
const rl = readline.createInterface({
    input: fs.createReadStream('./data/file1.txt')
})
rl.on('line', function(line){})
rl.on('close', function() {})

观察者模式 vs 发布订阅模式

两种模式的区别

观察者说明是一个被动的观察者,我什么时候执行不是由观察者说了算,我们就在那看,什么时候该我执行了就会有人通知我,我然后去执行对应的逻辑。就跟点击事件一样,至于点没点击不是我能控制的,我就在那看着,什么时候用户点击了,我就执行对应的逻辑。

发布订阅模式,说明我既可以发布也可以订阅。它是观察者模式的另一种实现形式,在实际工作中不用区分的那么细致。

// 绑定
event.on('event-key', () => {
    // 事件1
})
event.on('event-key', () => {
    // 事件2
})

// 触发执行
event.emit('event-key')

在发布订阅模式中,我既可以订阅也可以触发执行。

image.png

观察者模式

Subject 和 Observer 直接绑定,中间无媒介。如点击事件,事件直接和按钮进行绑定。

发布订阅模式

Publisher 和 Observer 相互不认识,中间有媒介。如在 A 组件中绑定一个事件,在 B 组件中触发这个事件,这个两个组件相隔十万八千里互补认识,那么就通过中间event这个媒介来通讯。

一个很明显的特点:发布订阅模式需要在代码中触发 emit ,而观察者模式没有 emit

发布订阅模式使用场景

自定义事件

Vue2 实例本身就支持自定义事件,但 Vue3 不再支持。Vue3 推荐使用 mitt ,轻量级 200 bytes ,文档 github.com/developit/m…

import mitt from 'mitt'
const emitter = mitt() // 工厂函数
emitter.on('change', () => {
    console.log('change1')
})
emitter.on('change', () => {
    console.log('change2')
})
emitter.emit('change')

这里把事件绑定和触发写在一个组件中了,也可以在一个组件中绑定事件,在另一个组件中触发,因为mitt在全局只有一个实例(单例模式)。

【注意】

在 Vue 和 React 组件中使用,在组件销毁之前,要及时 off 自定义事件。否则可能会导致内存泄漏。另,off 时要传入原来的函数,而不能是匿名函数。

postMessage 通讯

main.html

<body>
    <p>
        main page
        <button id="btn1">发送消息</button>
    </p>

    <iframe id="iframe1" src="./child.html"></iframe>

    <script>
        document.getElementById('btn1').addEventListener('click', () => {
            // contentWindow就是子页面的window对象
            window.iframe1.contentWindow.postMessage('hello', '*')
        })

        window.addEventListener('message', event => {
            console.log('main received', event.data)
        })
    </script>
</body>

child.html

<body>
    <p>
        child page
        <button id="btn1">发送消息</button>
    </p>

    <script>
        document.getElementById('btn1').addEventListener('click', () => {
           // 通过parent拿到父页面的window对象
            window.parent.postMessage('world', '*')
        })

        window.addEventListener('message', event => {
            console.log('child received', event.data)
        })
    </script>
</body>

【注意】

  • postMessage的第二个参数,可以限制域名,如发送敏感信息,要限制域名。
  • 可使用 event.origin 来判断信息来源是否合法,可选择不接受。
window.addEventListener('message', event => {
    console.log('origin', event.origin) // 通过 origin 判断是否来源合法
    console.log('child received', event.data)
})