Fork me on GitHub

前端设计模式之【观察者】

设计模式之观察者模式!

一、什么是观察者模式——发布订阅者模式?

观察者模式定义了一种一对多的依赖关系,让多个观察对象同时监听某一目标对象,当这一个对象的状态发生改变时,会通知所有的观察者对象,它们会自动更新。

发布订阅者模式分为两种角色,分为发布者和订阅者。首先创建一个发布者,然后关联订阅者,发布者发送通知,订阅者就可以得到相应的通知。

img

二、发布者

用面向对象的思想来说,发布者和订阅者就是两个类。

发布者的功能有以下几个:

  • 增加/移除订阅者
  • 通知订阅者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 定义发布者类
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)
})
}
}

三、订阅者

订阅者比较被动,被通知和被执行。

订阅者的功能如下:

  • 收到通知会更新响应的视图(任务)
1
2
3
4
5
6
7
8
9
10
// 定义订阅者类
class Observer {
constructor() {
console.log('Observer created')
}

update() {
console.log('Observer.update invoked')
}
}

四、实例

在实际的业务开发中,我们所有的定制化的发布者/订阅者逻辑都可以基于这两个基本类来改写。比如我们可以通过拓展发布者类,来使所有的订阅者来监听某个特定状态的变化

发布者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义一个具体的需求文档(prd)发布类
class PrdPublisher extends Publisher {
constructor() {
super()
// 状态
this.prdState = null
// 订阅者聚集地
this.observers = []
console.log('PrdPublisher created')
}

// 查看当前的状态
getState() {
console.log('PrdPublisher.getState invoked')
return this.prdState
}

// 该方法用于改变prdState的值
setState(state) {
console.log('PrdPublisher.setState invoked')
// prd的值发生改变
this.prdState = state
// 需求文档变更,立刻通知所有开发者
this.notify()
}
}

订阅者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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...')
}
}

五、观察者模式在面试中的考点

5.1 Vue 数据双向绑定的原理

面试:说说你对 vue 双向绑定的理解。

在 Vue 数据双向绑定的实现逻辑,分为以下三个角色:

  • Observer(监听器)—— 发布者:Vue 的双向绑定中,observer 是一个监听器,不仅有接收作用还有转发的功能,所以不仅是“订阅者”也是“发布者”。
  • watcher(订阅者)—— 订阅者: observer 会把监听到的数据转发给 watcher 对象(订阅者),watcher 接收到通知之后就会更新视图。
  • complie(编译器)—— 初始化视图:MVVM 框架特有的角色,负责对每个节点元素指令进行扫描和解析,指令的数据初始化、订阅者的创建都交给 complie 来做。

代码实现

Observer

首先实现一个方法,先对监听的数据对象进行遍历,然后给它加上 settergetter 方法,用来监听属性的改变。当对象的属性发生改变的时候,就会触发 setter 方法,然后就会通知订阅者做出视图更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 遍历所有的对象,对对象的每个属性进行设定 setter 方法
function observe(target) {
// 若target是一个对象,则遍历它
if(target && typeof target === 'object') {
Object.keys(target).forEach((key)=> {
// defineReactive方法会给目标属性装上“监听器”
defineReactive(target, key, target[key])
})
}
}

// 给对象属性安装监听器
function defineReactive(target, key, val) {
// 订阅者实例
const dep = new Dep()

// 监听当前属性
observe(val)

Object.defineProperty(target, key, {
set: (value) => {
// 通知所有订阅者
dep.notify()
}
})
}
发布者 Dep

主要用来增加订阅者、通知订阅者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Dep {
constructor() {
// 初始化订阅队列
this.subs = []
}

// 增加订阅者
addSub(sub) {
this.subs.push(sub)
}

// 通知订阅者(是不是所有的代码都似曾相识?)
notify() {
this.subs.forEach((sub)=>{
sub.update()
})
}
}

5.2 实现一个 Event Bus(Vue) 全局事件总线和 Event Emitter(Node)

具体的说,Event Bus 是一种发布——订阅者模式。日常运用非常的广泛,也属于设计模式中的重中之重。

1、 Event Bus 在 Vue 中的应用

全局事件总线主要起到一个中心沟通转发作用,可以作为一个事件中心。如果组件 A 和组件 B 进行通信,除了使用 Vuex 外,可以使用 Event Bus。但是是通过事件中心来沟通,不能进行私下的通信。

创建一个 Event Bus 实例:

1
2
const EventBus = new Vue()
export default EventBus

主文件引入 Event Bus,挂在全局:

1
2
import bus from 'EventBus的文件路径'
Vue.prototype.bus = bus

订阅事件:

1
2
// 这里func指someEvent这个事件的监听函数
this.bus.$on('someEvent', func)

发布事件:

1
2
// 这里params指someEvent这个事件被触发时回调函数接收的入参
this.bus.$emit('someEvent', params)

PS:所有事件的发布/订阅操作,必须经由事件中心,禁止一切“私下交易”!

2、实现一个 Event Bus

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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.apply(...args)
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}

六、观察者模式和订阅者模式的区别?

观察者和订阅者之间最大的区别就是是否存在于第三方。

  • 观察者模式: 发布者直接触及到订阅者的操作叫做观察者模式。
  • 发布—订阅者模式: 发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作叫做发布订阅者模式。

6.1 两者的适用条件

观察者模式

观察者模式,解决的是模块间的耦合问题,即便是两个分离的、毫不相关的模块,也可以实现数据通信。但是,观察者模式只是减少了耦合问题,但并没完全解决耦合。 —— 被观察者还要维护观察者的集合,观察者必须提供统一的接口供被观察者调用。

发布订阅者模式

发布订阅者模式无序关心对方,而是都交给中心事件总线进行处理,实现了完全的解耦。

适用条件
  • 在实际开发中,模块化的解耦诉求并非总是要进行完全解耦。如果两个模块之间本身存在关联,且这种关联是稳定的、必要的,那么我们使用观察者模式就足够了。

  • 而在模块与模块之间独立性较强、且没有必要单纯为了数据通信而强行为两者制造依赖的情况下,我们往往会倾向于使用发布-订阅模式。