摘要:源码解读设计模式系列文章将陆陆续续进行更新中 ~
摘要:源码解读设计模式系列文章将陆陆续续进行更新中 ~
观察者模式
首先话题下来,我们得反问一下自己,什么是观察者模式?
概念
观察者模式(Observer):通常又被称作为发布-订阅者模式。它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合。
讲个故事
上面对于观察者模式的概念可能会比较官方化,所以我们讲个故事来理解它。
- A:是密探,代号 001(发布者)
- B:是通信人员,负责与 A 进行秘密交接(订阅者)
- A 日常工作就是在明面采集一些情报
- B 则负责暗中观察着 A
- 一旦 A 传递出一些有关消息(更多时候需要对消息进行封装传递,后面根据源码具体分析)
- B 会立马订阅到该消息,然后做一些相对应的变更,比如说通知做一些事情应对的一些动作。
适用性
以下任一场景都可以使用观察者模式
- 当一个抽象模型有两个方面,其中一个方面依赖于另一方面。讲这两者封装在独立的对象中可以让它们可以各自独立的改变和复用
- 当一个对象的改变的时候,需要同时改变其它对象,但是却不知道具体多少对象有待改变
- 当一个对象必须通知其它对象,但是却不知道具体对象到底是谁。换句话说,你不希望这些对象是紧密耦合的。
vue 对于观察者模式的使用
vue
使用到观察者模式的地方有很多,这里我们主要谈谈对于数据初始化这一块的。
var vm = new Vue({
data () {
return {
a: 'hello vue'
}
}
})
1、实现数据劫持
上图我们可以看到,vue
是利用的是 Object.defineProperty()
对数据进行劫持。 并在数据传递变更的时候封装了一层中转站,即我们看到的 Dep
和 Watcher
两个类。
这一小节,我们只看如何通过观察者模式对数据进行劫持。
1.1、递归遍历
我们都知道,vue
对于 data
里面的数据都做了劫持的,那只能对对象进行遍历从而完成每个属性的劫持,源码具体如下
walk (obj: Object) {
const keys = Object.keys(obj)
// 遍历将其变成 vue 的访问器属性
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
参考vue源码视频讲解:进入学习
1.2、发布/订阅
从上面对象的遍历我们看到了 defineReactive
,那么劫持最关键的点也在于这个函数,该函数里面封装了 getter
和 setter
函数,使用观察者模式,互相监听
// 设置为访问器属性,并在其 getter 和 setter 函数中,使用发布/订阅模式,互相监听。
export function defineReactive (
obj: Object,
key: string,
val: any
) {
// 这里用到了观察者(发布/订阅)模式进行了劫持封装,它定义了一种一对多的关系,让多个观察者监听一个主题对象,这个主题对象的状态发生改变时会通知所有观察者对象,观察者对象就可以更新自己的状态。
// 实例化一个主题对象,对象中有空的观察者列表
const dep = new Dep()
// 获取属性描述符对象(更多的为了 computed 里面的自定义 get 和 set 进行的设计)
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
const getter = property && property.get
const setter = property && property.set
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 收集依赖,建立一对多的的关系,让多个观察者监听当前主题对象
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
// 这里是对数组进行劫持
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
// 劫持到数据变更,并发布消息进行通知
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})
}
1.3、返回 Observer 实例
上面我们看到了observe
函数,核心就是返回一个 Observer
实例
return new Observer(value)
2、消息封装,实现 “中转站”
首先我们要理解,为什么要做一层消息传递的封装?
我们在讲解观察者模式的时候有提到它的 适用性
。这里也同理,我们在劫持到数据变更的时候,并进行数据变更通知的时候,如果不做一个"中转站"的话,我们根本不知道到底谁订阅了消息,具体有多少对象订阅了消息。
这就好比上文中我提到的故事中的密探 A(发布者) 和 B(订阅者)。密探 A 与 B 进行信息传递,两人都知道对方这么一个人的存在,但密探 A 不知道具体 B 是谁以及到底有多少(订阅者)订阅着自己,可能很多都订阅着密探 A 的信息,so 密探 A(发布者) 需要通过暗号
收集到所有订阅着其消息的(订阅者),这里对于订阅者的收集其实就是一层封装
。然后密探 A 只需将消息发布出去,而订阅者们接受到通知,只管进行自己的 update
操作即可。
简单一点,即收集完订阅者们的密探 A 只管发布消息, B 以及更多的只管订阅消息并进行对应的 update
操作,每个模块确保其独立性,实现高内聚低耦合
这两大原则。
废话不多说,我们接下来直接开始讲 vue 是如何做的消息封装的
2.1、Dep
Dep
,全名 Dependency,从名字我们也能大概看出 Dep
类是用来做依赖收集的,具体怎么收集呢。我们直接看源码
let uid = 0
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
// 用来给每个订阅者 Watcher 做唯一标识符,防止重复收集