观察者模式
一对多关系解耦的行为设计模式,主要涉及两个角色,观察者和观察目标
观察者直接订阅观察目标,观察目标(订阅中心)做出通知,观察者就要进行处理
vue依赖收集
每个组件实例都会有相应的Watcher实,渲染组件的过程,会把属性记录为依赖,当我们操作数据的时候,依赖项的setter会被调用,从而通知Watcher重新计算,从而使得相关的组件得以更新。
Getter里进行依赖收集,当依赖的数据被更新时,会触发该数据的setter,setter里会触发render函数重新计算
依赖收集与观察者模式
vue依赖收集的场景既是一对多,一个数据发生变更,多处用到该数据的地方都得处理。而且依赖的数据变了,即必须要做出处理。
vue里,依赖的数据是观察目标(Dep, 订阅中心),依赖就是Dep
视图、计算属性、侦听器等算是观察者(watcher),用到该数据的就是观察者(Watcher)
源码解析vue依赖收集
三个角色
Dep: 扮演观察目标的角色,每一个数据都会有一个Dep类的实例,内部有subs队列,保存着依赖本数据的观察者Watcher,当本数据变更时,调用实例dep.notify通知观察者Watcher
Watcher: 扮演观察者的角色,进行观察者函数 的包装处理,render函数会被包装成一个Watcher实例
Observer: 辅助的可观测类,数组和对象通过其转化,可成为可观测数据
每一个数据都会有一个Dep类的实例:观察目标
1 | let uid = 0 |
由于JavaScript是单线程模型,所以虽然有多个观察者函数,但是一个时刻内,就只会有一个观察者函数在执行,那么此刻正在执行的那个观察者函数,所对应的Watcher实例,便会被赋给Dep.target这一类变量,从而只要访问Dep.target就能知道当前的观察者是谁。在后续的依赖收集工作里,getter里会调用dep.depend(),而setter里则会调用dep.notify();
配置观测数据
observe主要是给每层数据加一个私有属性ob,同时这个ob是 Observer类的一个实例,总结来说就是给对象和数组实例化一个Observer实例,且在依赖的数据是可配置的时候才会实例化。
在ob上挂载dep实例,用来处理改变内容的情况,以便能够形成追踪链路。
1 | function observe (value, asRootData) { |
Observer类代码: 辅助的可观测类,数组和对象通过其转化,可成为可观测数据
1 | var Observer = function Observer (value) { |
创建一个Dep管理当前依赖 (这里我们可以叫大管家dep后续会讲到为什么是大管家)
区分数组还是对象做不同的响应式处理
对象的情况直接调用walk方法
数组的情况需要判断是否有原型 (老的ie是没有的)
在数组有原型的情况直接调protoAugment进行原型覆盖(这里主要是为了数组的增删改因为Object.defineProperty在语言层面检测不到数组的变化所以需要自己覆盖下数组的原型使用自己的方法检测)
在数组没有原型的情况调copyAugment复制一份Observer类就是: 将Observer实例挂载在ob属性上,提供后续观测数据使用。同时实例化一个Dep(观察目标)类实例。同时将对象、数组绑定在value对象上,即
1 | // 原数据 |
如果value是个对象,就执行walk()过程,遍历对象把每一项数据都变为可观测数据(调用defineReactive方法处理)
Observer里依赖收集:
1 | // 数据改造成属性getter/setter |
defineReactive方法做了下面几件事
- 为每个属性实例化一个dep依赖收集器,用于收集该属性的相关依赖(这里的dep我们可以看成 小管家dep只管理自己当前的依赖)
- 缓存属性原有的get和set方法,保证后面重写get、set方法时行为正常
- 对子属性进行observer递归
- 将对象中的每一个属性都加上getter、setter方法
这里就是通过Object.defineProperty将数据变成响应式的过程并且和watcher创建依赖关系
经过defineReactive处理的数据变化如下, 每个属性都有自己的dep、childOb、getter、setter,并且每个object类型的属性都有ob
getter方法
1 | get: function reactiveGetter () { |
做了2件事
1.调用属性的getter方法返回值
2.收集依赖
a.Dep.target表示一个依赖,即观察者Watcher类的实例,大部分情况下是一个依赖函数。
b.如果存在需要被收集的依赖,则收集依赖的数据到该属性的dep依赖收集器中,每个属性都有一个dep(实例化的Dep)属性
c.如果存在childOb,则依赖收集到value.ob.dep.depend();
d.如果属性的值是数组,调用dependArray函数,将依赖收集到数组中的每一个对象元素的ob.dep中。确保在使用this.$set 或 Vue.set时,数组中嵌套的对象能正常响应。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12const data = {
user: [
name: 'cpp'
]
}
// 页面显示
{{user}}
<Button @click="addAge()">addAge</Button>
//addAge方法,为数组中的嵌套对象添加age属性
addAge: function(){
this.$set(this.user[0], 'age', 18)
}
dependArray函数如下:
1 | function dependArray (value) { |
转化后的数据
1 |
|
(所谓依赖,就是这个组件所需要依赖到的数据)
总结一句话就是在getter方法中,将存在的观察者Watcher收集到Dep中,由Dep中的subs统一管理,集中维护
如果value是个数组,就执行observeArray()过程,递归地对数组元素调用observe(),以便能够对元素还是数组的情况进行处理.
相关代码如下
1 | if (Array.isArray(value)) { |
1.使用protoAugment方法指定value值的原型链为arrayMethods函数,
2.遍历数组中的每一个值,调用observer方法,使其观测
arrayMethods 拦截修改数组方法
1 | var arrayProto = Array.prototype; |
arrayMethods做了几件事
1.需要拦截的修改数组的方法有:push、pop、shift、unshift、splice、sort、reverse
2.当数组有新增元素时,使用observeArray对新增的元素进行观测
3.拦截数组的方法,当修改数组方法被调用时触发数组中的ob.dep的所有依赖(ob.dep.notify();)
Watcher
只关心数据,数据发生变化的时候,Dep.notify() => Watcher.update()
Watcher扮演的角色是观察者,它关心数据,在数据变化后能够获得通知,并作出处理。一个组件里可以有多个Watcher类实例,Watcher类包装观察者函数,而观察者函数使用数据。 观察者函数经过Watcher是这么被包装的:
- 模板渲染:
this._watcher = new Watcher(this, render, this._update)
- 计算属性:Watcher主要代码如下
1
2
3
4
5
6
7
8
9
10
11computed: {
name() {
return `${this.firstName} ${this.lastName}`;
}
}
/*
会包装成
new Watcher(this, function name() {
return `${this.firstName} ${this.lastName}`
}, callback);
*/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
49
50
51class Watcher {
constructor(
vm: Component, // 组件实例
expOrFn: string | Function, // 观察者函数 即上述的function name函数
cb: Function, // 回调函数
options?: ?Object, // 选项
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.deps = [] // 缓存上一轮执行观察者函数用到的dep实例
this.newDeps = [] // 存储本轮执行观察者函数用到的dep实例
this.depIds = new Set() // Hash表,用于快速查找
this.newDepIds = new Set() // Hash表,用于快速查找
}
// 视图更新
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
// 订阅观察目标
addDep(dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
}