# 观察者模式初相识
观察者模式
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。 —— Graphic Design Patterns
观察者模式有一个"别名"叫 - 发布-订阅模式,这个别名是加引号的,这两者之间还是有区别的,但是从这个别名我们可以得到个关键的信息 - 观察者模式需要有发布者和订阅者两个核心角色组成。
上面都是一些理论知识,对于一些没有基础的同学可能理解起来比较困难。下面我们来举一个生活中的例子,希望能帮大家更好地理解。
比如今天是你入职的第一天,你的直属领导会把你拉到工作群里(如果你试用期没过,他也会把你移出工作群)方便以后的工作。过了一会儿,你的领导在群里艾特了所有人,通知大家10:30开会。因为现在还没有到时间,大家先不用去开会。但是呢,因为接受到了这个通知,所以大家时不时的都要看一下表,等到10:30一到,一起去会议室开会,但是你的直属领导可能在10:30有其他紧急的事情要处理,这个时候到,会把开会时间改为其他时段,并在群里通知大家,这个过程就是一个典型的观察者模式。
在上面这个例子中,你的直属领导就是发布者,在工作群里的其他同事就是订阅者,也就是观察者对象。简单总结下。观察者模式的套路:角色划分 -> 状态变化 -> 发布者通知订阅者。
从上面的例子我们可以看出,发布者有三个最基本的功能分别是:增加订阅者,删除订阅者和通知订阅者。
// 定义发布者类
class Publisher {
constructor() {
this.observers = []
}
add(observer) {
this.observers.push(observer)
}
remove(observer) {
this.observers = this.observers.filter(item => item !== observer)
}
notify() {
this.observers.forEach(observer => observer.update(this))
}
}
订阅者有两个基本的职能分别是被通知和去执行(接受发布者的调用)。
class Observer {
constructor() {
console.log('Observer created')
}
update() {
console.log('Observer.update invoked')
}
}
上面我们就完成了最基本的发布者和订阅者类的设计和编写,但是呢,还缺少一个非常重要的环节 - 作为发布者,你到底发布了啥?作为订阅者,我又订阅了啥?下面我们来完善一下。
// 定义一个具体的需求文档(prd)发布类
class MeetingPublisher extends Publisher {
constructor() {
super()
// 初始化会议时间
this.meetingTime = null
// 直属领导还没拉群,群目前为空
this.observers = []
console.log('MeetingPublisher created')
}
// 该方法用于获取当前的prdState
getState() {
console.log('MeetingPublisher.getState invoked')
return this.meetingTime
}
// 该方法用于改变 mettingTime 的值
setState(state) {
console.log('MeetingPublisher.setState invoked')
// 会议时间的值发生改变
this.mettingTime = state
// 会议时间变更,立刻通知所有成员
this.notify()
}
}
参会人员接受开会时间,并去开会。
class AttendeesObserver extends Observer {
constructor() {
super()
// 会议时间还不存在,初始为空字符串
this.meetingTime = ''
console.log('DeveloperObserver created')
}
// 重写一个具体的update方法
update(publisher) {
console.log('DeveloperObserver.update invoked')
// 更新开会时间
this.meetingTime = publisher.getState()
// 调用开会函数
this.metting()
}
// metting方法,一个开会的方法
metting() {
// 获取开会时间
const meetingTime = this.meetingTime
// 开始开会。。。
...
console.log('bala bala bala...')
}
}
上面我们也就实现了,目标对象(开会时间)的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。下面我们来看一下它的整体流程:
// 创建与会者 a、b、c
const a = new AttendeesObserver();
const b = new AttendeesObserver();
const c = new AttendeesObserver();
// 创建会议发起者
const manager = new MeetingPublisher();
// 开会时间
const meetingTime = '10:30'
// 拉群
manager.add(a);
manager.add(b);
manager.add(c);
// 设置会议时间
manager.setState(meetingTime);
上面就是对观察者模式的一个简要概述,希望对大家有帮助。
# 应用实践
下面我们来看一下观察者模式的真实应用场景。
# Vue数据双向绑定(响应式系统)的实现原理
关于Vue的响应式原理可以看下官方文档深入响应式原理 (opens new window),这里仅做简要说明。
在 Vue 中,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新——这是一个典型的观察者模式。

Vue数据双向绑定的实现逻辑里有三个重要角色:
- observer(监听器):注意,此 observer 非彼 observer。在我们上节的解析中,observer 作为设计模式中的一个角色,代表“订阅者”。但在Vue数据双向绑定的角色结构里,所谓的 observer 不仅是一个数据监听器,它还需要对监听到的数据进行转发——也就是说它同时还是一个发布者。
- watcher(订阅者):observer 把数据转发给了真正的订阅者——watcher对象。watcher 接收到新的数据后,会去更新视图。
- compile(编译器):MVVM 框架特有的角色,负责对每个节点元素指令进行扫描和解析,指令的数据初始化、订阅者的创建这些“杂活”也归它管
observer 的实现:
// observe方法遍历并包装对象属性
function observe(target) {
// 若target是一个对象,则遍历它
if(target && typeof target === 'object') {
Object.keys(target).forEach((key)=> {
// defineReactive方法会给目标属性装上“监听器”
defineReactive(target, key, target[key])
})
}
}
// 定义defineReactive方法
function defineReactive(target, key, val) {
// 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历
observe(val)
// 为当前属性安装监听器
Object.defineProperty(target, key, {
// 可枚举
enumerable: true,
// 不可配置
configurable: false,
get: function () {
return val;
},
// 监听器函数
set: function (value) {
console.log(`${target}属性的${key}属性从${val}值变成了了${value}`)
val = value
}
});
}
订阅者 Dep 的实现:
// 定义订阅者类Dep
class Dep {
constructor() {
// 初始化订阅队列
this.subs = []
}
// 增加订阅者
addSub(sub) {
this.subs.push(sub)
}
// 通知订阅者(是不是所有的代码都似曾相识?)
notify() {
this.subs.forEach((sub)=>{
sub.update()
})
}
}
在监听器中通知订阅者:
function defineReactive(target, key, val) {
const dep = new Dep()
// 监听当前属性
observe(val)
Object.defineProperty(target, key, {
set: (value) => {
// 通知所有订阅者
dep.notify()
}
})
}
# Event Bus
全局事件总线严格来说不是观察者模式,而是发布-订阅模式。
我们来简单看下 Event Bus 在 Vue 中的使用:
const EventBus = new Vue()
export default EventBus
import bus from 'EventBus的文件路径'
Vue.prototype.bus = bus
订阅事件:
// 这里func指someEvent这个事件的监听函数
this.bus.$on('someEvent', func)
发布事件:
// 这里params指someEvent这个事件被触发时回调函数接收的入参
this.bus.$emit('someEvent', params)
class EventEmitter {
constructor() {
// handlers是一个map,用于存储事件与回调之间的对应关系
this.handlers = {}
}
// on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
on(eventName, cb) {
// 先检查一下目标事件名有没有对应的监听函数队列
if (!this.handlers[eventName]) {
// 如果没有,那么首先初始化一个监听函数队列
this.handlers[eventName] = []
}
// 把回调函数推入目标事件的监听函数队列里去
this.handlers[eventName].push(cb)
}
// emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
emit(eventName, ...args) {
// 检查目标事件是否有监听函数队列
if (this.handlers[eventName]) {
// 如果有,则逐个调用队列里的回调函数
this.handlers[eventName].forEach((callback) => {
callback(...args)
})
}
}
// 移除某个事件回调队列里的指定回调函数
off(eventName, cb) {
const callbacks = this.handlers[eventName]
const index = callbacks.indexOf(cb)
if (index !== -1) {
callbacks.splice(index, 1)
}
}
// 为事件注册单次监听器
once(eventName, cb) {
// 对回调函数进行包装,使其执行完毕自动被移除
const wrapper = (...args) => {
cb(...args)
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}
# 发布-订阅者模式和观察者模式的区别
发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式。观察者模式和发布-订阅模式之间的区别,在于是否存在第三方、发布者能否直接感知订阅者。
观察者模式是为了减少模块间的耦合,但是并没有完全解决耦合问题。发布-订阅模式,事件的注册和触发独立于第三方平台上,实现了完全解耦。
Vue 在哪个生命周期发送请求 Vue组件生命周期执行顺序 Vue组件通信 v-if和v-for为什么不建议一起使用 虚拟dom的优缺点 vue中key的作用 keep alive的使用场景和原理