什么是观察者模式
你在星巴克点了咖啡,此时你并不需要在吧台坐等,你只需要回到位子上玩手机,等咖啡好了服务员会叫你。这就是观察者模式。
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
从图中可以看到主题和观察者是1:n的形式,1:n说明它的扩展性非常好,它可以是1:2,也可以是1:3。
观察者模式应用场景
Vue watch
{
data() {
name: '双越'
},
watch: {
name(newVal, val) {
console.log(newValue, val)
}
}
}
data就是主题,watch里面的函数就是观察者,当主题data发生变化的时候,观察者就执行对应的操作。
Vue 组件更新过程
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')
在发布订阅模式中,我既可以订阅也可以触发执行。
观察者模式
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)
})