观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象, 当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。 —— Graphic Design Patterns
生活中的观察者模式
比如当我们进入一个聊天室 / 群,如果有人在聊天室发言,那么这个聊天室里的所有人都会收到这个人的发言。这是一个典型的发布 - 订阅模式,当我们加入了这个群,相当于订阅了在这个聊天室发送的消息,当有新的消息产生,聊天室会负责将消息发布给所有聊天室的订阅者。
角色划分 —> 状态变化 —> 发布者通知到订阅者,这就是观察者模式的“套路”。
定义
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象
总述:
主要有下面几个概念:
- Publisher :发布者
- Subscriber :订阅者
- SubscriberMap :持有不同 type 的数组,存储有所有订阅者的数组
- type :消息类型,订阅者可以订阅的不同消息类型
- subscribe :添加订阅者
- unSubscribe :删除订阅者
- notify :该方法遍历通知 SubscriberMap 中对应 type 的每个订阅者
结构如下图:
发布者类
基本技能包括:
- 是拉群(增加订阅者),
- @所有人(通知订阅者),
踢走项目组成员(移除订阅者)
// 定义发布者类class Publisher {constructor() {this.observers = [] // 订阅者队列console.log('Publisher created')}// 增加订阅者add(observer) {console.log('Publisher.add invoked')this.observers.push(observer)}// 移除订阅者remove(observer) {console.log('Publisher.remove invoked')this.observers.forEach((item, i) => {if (item === observer) {this.observers.splice(i, 1)}})}// 通知所有订阅者notify() {console.log('Publisher.notify invoked')this.observers.forEach((observer) => {observer.update(this)})}}
订阅者
- 被通知
- 去执行(本质上是接受发布者的调用,这步我们在Publisher中已经做掉了)。
// 定义订阅者类class Observer {constructor() {console.log('Observer created')}update() {console.log('Observer.update invoked')}}
实践
基于基类来定制自己的发布者/订阅者逻辑。如:让订阅者们来监听需求文档(prd)的变化:
发布者:
// 定义一个具体的需求文档(prd)发布类class PrdPublisher extends Publisher {constructor() {super()// 初始化需求文档this.prdState = null// 韩梅梅还没有拉群,开发群目前为空this.observers = []console.log('PrdPublisher created')}// 该方法用于获取当前的prdStategetState() {console.log('PrdPublisher.getState invoked')return this.prdState}// 该方法用于改变prdState的值setState(state) {console.log('PrdPublisher.setState invoked')// prd的值发生改变this.prdState = state// 需求文档变更,立刻通知所有开发者this.notify()}}
订阅方:
接收需求文档、并开始干活
class DeveloperObserver extends Observer {constructor() {super()// 需求文档一开始还不存在,prd初始为空对象this.prdState = {}console.log('DeveloperObserver created')}// 重写一个具体的update方法update(publisher) {console.log('DeveloperObserver.update invoked')// 更新需求文档this.prdState = publisher.getState()// 调用工作函数this.work()}// work方法,一个专门搬砖的方法work() {// 获取需求文档const prd = this.prdState// 开始基于需求文档提供的信息搬砖。。。...console.log('996 begins...')}}
工作:
- PrdPublisher 对象,可以通过调用 setState 方法来更新需求文档。
- 需求文档每次更新,都会紧接着调用 notify 方法来通知所有开发者,当然也可特定的逻辑通知到个别订阅**者**:
目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
// 创建订阅者:前端开发李雷const liLei = new DeveloperObserver()// 创建订阅者:服务端开发小A(sorry。。。起名字真的太难了)const A = new DeveloperObserver()// 发布者:韩梅梅出现了const hanMeiMei = new PrdPublisher()// 需求文档出现了const prd = {// 具体的需求内容...}// 韩梅梅开始拉群hanMeiMei.add(liLei)hanMeiMei.add(A)// 韩梅梅发送了需求文档,并@了所有人hanMeiMei.setState(prd)
Vue数据双向绑定(响应式系统)的实现原理
解析
过程分析

在 Vue 中,组件渲染函数(Render Function)被执行前,会对数据层的数据进行响应式化,即:
- 使用
Object.defineProperty把数据转为getter/setter,并为每个数据添加一个订阅者列表
这个列表是 getter 闭包中的属性,将会记录所有依赖这个数据的组件。
响应式化后的数据相当于发布者。
- 每个
组件实例都有相应的watcher实例对象,它会在组件渲染的过程中将本组件的 Watcher 放到自己所依赖的响应式数据的订阅者列表里,这就相当于完成了订阅,一般这个过程被称为依赖收集。
**
- 当响应式数据发生变化的时候,会触发
setter,setter会负责通知(Notify)该数据的订阅者列表里的Watcher,Watcher 会触发组件**重新渲染(Trigger re-render)来更新(update)视图**。
这是一个典型的观察者模式。
在Vue数据双向绑定的实现逻辑里,有这样三个关键角色:
observer(监听器):注意,此 observer 非 “订阅者”。- 在Vue数据双向绑定的角色结构里,所谓的 observer 不仅是一个数据监听器,它还需要对监听到的数据进行转发——也就是说它同时还是一个**发布者**。
watcher(订阅者):observer 把数据转发给了真正的订阅者——watcher**对象**。watcher 接收到新的数据后,会去更新视图。compile(编译器):MVVM 框架特有的角色,负责对每个节点元素指令进行扫描和解析,指令的数据初始化、订阅者的创建这些“杂活”也归它管。
这三者的配合过程如图所示:
源码分析
Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter() {...// 如果原本对象拥有getter方法则执行const value = getter ? getter.call(obj) : valdep.depend() // 进行依赖收集,dep.addSubreturn value},set: function reactiveSetter(newVal) {...// 如果原本对象拥有setter方法则执行if (setter) { setter.call(obj, newVal) }dep.notify() // 如果发生变更,则通知更新}})
而这个 dep 上的 depend 和 notify 就是订阅和发布通知的具体方法。
核心代码
实现observer
- 对需要监听的数据对象进行遍历;
- 给它的属性加上定制的
getter和setter函数。- 当这个对象的某个属性发生了改变,就会触发 setter 函数,进而通知到订阅者。
这个 setter 函数,就是我们的监听器:
// 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
// 定义订阅者类Depclass Dep {constructor() {// 初始化订阅队列this.subs = []}// 增加订阅者addSub(sub) {this.subs.push(sub)}// 通知订阅者notify() {this.subs.forEach((sub)=>{sub.update()})}}
改写 defineReactive
改写setter 方法,在监听器里去通知订阅者
function defineReactive(target, key, val) {const dep = new Dep()// 监听当前属性observe(val)Object.defineProperty(target, key, {set: (value) => {// 通知所有订阅者dep.notify()}})}
Event Bus/ Event Emitter
Event Bus/Event Emitter 作为全局事件总线,它起到的是一个沟通桥梁的作用
所有事件的订阅/发布都不能由订阅方和发布方“私下沟通”,必须要委托这个事件中心帮我们实现。
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)}}
在React中使用Event Bus来实现组件间的通讯
观察者模式与发布-订阅模式的区别是什么?
观察者模式:
发布者直接触及到订阅者的操作:
发布-订阅模式:
发布者不直接触及到订阅者,而是由统一的第三方来完成实际的通信的操作:
- 韩梅梅没有拉群,而是把需求文档上传到了公司统一的需求平台上,需求平台感知到文件的变化、自动通知了每一位订阅了该文件的开发者
- 通过
EventBus去实现事件监听/发布

解决的问题:
两者都解决了模块间的耦合问题,
- 两个分离的、毫不相关的模块,也可以实现数据通信。
- 监听事件的位置和触发事件的位置是不受限的,只要它们在同一个上下文里,就能够彼此感知。
观察者模式:

