Skip to content
微信公众号

观察者模式

介绍

观察者模式是前端最常用的一个设计模式,也是 UI 编程最重要的思想。

示例

例如你在星巴克点了咖啡,此时你并不需要在吧台坐等,你只需要回到位子上玩手机,等咖啡好了服务员会叫你。不光叫你,其他人的咖啡好了,服务员也会叫他们来取。

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

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

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

还有,Vue React 的生命周期,也是观察者模式

演示

Subject 和 Observer 是一对多的关系

ts
// 主题
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 (const 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

是否符合设计原则?

5 大设计原则中,最重要的就是:开放封闭原则,对扩展开放,对修改封闭

  • Observer 和 Target 分离,解耦
  • Observer 可自由扩展
  • Target 可自由扩展

场景

观察者模式在前端(包括其他 UI 编程领域)应用非常广泛。

DOM 事件

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

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

Vue React 组件生命周期

PS:当你开发自己的 lib 时,也要考虑它的完整生命周期,如 wangEditor,负责创建,也得复杂销毁。

Vue watch

js
// Vue 组件配置
{
    data() {
        name: '测试'
    },
    watch: {
        name(newVal, val) {
            console.log(newValue, val)
        }
    }
}

PS:面试题 watch 和 watchEffect 有什么区别?

Vue 组件更新过程

PS:React 组件更新过程不是这样的,它是通过 setState 主动触发的,而非响应式监听。

各种异步的回调

定时器

setTimeout setInterval

Promise then 回调

参考之前 loadImg 代码

nodejs stream

js
const fs = require('fs')
const readStream = fs.createReadStream('./data/file1.txt')  // 读取文件的 stream

let length = 0
readStream.on('data', function (chunk) {
    length += chunk.toString().length
})
readStream.on('end', function () {
    console.log(length)
})

nodejs readline

js
const readline = require('readline');
const fs = require('fs')

const rl = readline.createInterface({
    input: fs.createReadStream('./data/file1.txt')
})

let lineNum = 0
rl.on('line', function(line){
    lineNum++
})
rl.on('close', function() {
    console.log('lineNum', lineNum)
})

nodejs http server 回调

js
const http = require('http')

function serverCallback(req, res) {
    console.log('get 请求不处理', req.url)
    res.end('hello')
}
http.createServer(serverCallback).listen(8081)
console.log('监听 8081 端口……')

MutationObserver

HTML 代码

html
<div id="container">
    <p>A</p>
    <p>B</p>
</div>

JS 代码

ts
function callback(records: MutationRecord[], observer: MutationObserver) {
    for (let record of records) {
        console.log('record', record)
    }
}
const observer = new MutationObserver(callback)

const containerElem = document.getElementById('container')
const options = {
    attributes: true, // 监听属性变化
    attributeOldValue: true, // 变化之后,记录旧属性值
    childList: true, // 监听子节点变化(新增删除)
    characterData: true, // 监听节点内容或文本变化
    characterDataOldValue: true, // 变化之后,记录旧内容
    subtree: true, // 递归监听所有下级节点
}

// 开始监听
observer.observe(containerElem!, options)

// 停止监听
// observer.disconnect()

vs 发布订阅模式

发布订阅模式,没有在传统 23 种设计模式中,它是观察者模式的另一个版本。

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

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

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

观察者模式

  • Subject 和 Observer 直接绑定,中间无媒介
  • addEventListener 绑定事件

发布订阅模式

  • Publisher 和 Observer 相互不认识,中间有媒介
  • event 自定义事件

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

场景

自定义事件

Vue2 实例本身就支持自定义事件,但 Vue3 不再支持。

Vue3 推荐使用 mitt ,轻量级 200 bytes ,文档 https://github.com/developit/mitt

ts
import mitt from 'mitt'

const emitter = mitt() // 工厂函数

emitter.on('change', () => {
    console.log('change1')
})
emitter.on('change', () => {
    console.log('change2')
})

emitter.emit('change')

但是,mitt 没有 once ,需要可以使用 event-emitter

ts
import eventEmitter from 'event-emitter' // 还要安装 @types/event-emitter

const emitter = eventEmitter()

emitter.on('change', (value: string) => {
    console.log('change1', value)
})
emitter.on('change', (value: string) => {
    console.log('change2', value)
})
emitter.once('change', (value: string) => {
    console.log('change3', value)
})

emitter.emit('change', '张三')
emitter.emit('change', '李四')

postMessage 通讯

通过 window.postMessage 发送消息。注意第二个参数,可以限制域名,如发送敏感信息,要限制域名。

js
// 父页面向 iframe 发送消息
window.iframe1.contentWindow.postMessage('hello', '*') 

// iframe 向父页面发送消息
window.parent.postMessage('world', '*')

可监听 message 来接收消息。可使用 event.origin 来判断信息来源是否合法,可选择不接受。

js
window.addEventListener('message', event => {
    console.log('origin', event.origin) // 通过 origin 判断是否来源合法
    console.log('child received', event.data)
})

同类型的还有

  • nodejs 多进程通讯
  • WebWorker 通讯
  • WebSocket 通讯

注意事项

在 Vue 和 React 组件中使用,在组件销毁之前,要及时 off 自定义事件。否则可能会导致内存泄漏

另,off 时要传入原来的函数,而不能是匿名函数。

本站总访问量次,本站总访客数人次
Released under the MIT License.