Vue 响应式原理
# 观察者模式
vue 响应式系统的实现其实使用了一种称作观察者模式
的设计模式。说到观察者模式,会涉及到两个很重要的角色:
- 被观察目标:上图中的响应式数据对象
Data
- 观察者:上图中的
Watcher
结合上图,可以将 Vue 的响应式系统的整体流程简单描述为下面的过程:
- 组件 render 过程中,会触发响应式数据(
Data
)的getter
,在这个过程中,就会完成观察者对被观察目标的依赖绑定,也就是常说的依赖收集
,而被观察目标就是响应式数据(Data
)本身(被观察目标与观察者之间是一对多
的关系,因为一个响应式数据可能被多个观察者依赖)- 一旦被观察的目标发生改变(比如重新赋值),会触发响应式数据的
setter
,然后在setter
中会通知(Notify
)到所有在上一步中绑定的观察者(Watcher
)。观察者收到通知后,会触发组件的重新渲染(Trigger re-render
)- 组件重新渲染又会调用组件的渲染方法(
Component Render Function
),这个过程会涉及到上一篇提到的针对新旧组件vnode 的 diff
以及vnode 到真实 DOM 的 patch
过程。
要实现上面的整个过程,有几个关键的节点,
- 响应式数据的创建
- 观察者与被观察目标的依赖关系绑定,即
依赖收集
- 被观察目标变更后如何通知到所有相关联的观察者,即
派发更新
Vue 源码中利用三个类实现了 依赖收集
以及派发更新
:
Observer
:辅助的可观测类,数组/对象通过它的转化,就会成为响应式数据Dep
:响应式数据的每一个属性都会拥有一个 Dep 类实例,Dep 实例内部有个subs
队列,保存着依赖本数据的观察者,当本数据变更时,调用dep.notify()
通知观察者Watcher
:扮演观察者的角色,进行观察者函数的包装处理。如render()
函数,会被包装成一个Watcher
实例
由此可见,Dep
其实是被观察者(Observer)与观察者(Watcher)之间的一座桥梁,
- 一方面,响应式数据的每一个 key 都会有一个与之绑定的 Dep 实例,该实例可以为响应式数据搜集所对应的观察者,并存放到 Dep 实例的 subs 数组中去
- 另一方面,当响应式数据发生变化的时候,就会通过与该响应式数据绑定的 Dep 实例,获取到所有相应的观察者,并通知它们进行更新操作
# 响应式数据的创建
在 vue 实例化过程中,有一步是对数据 state 的初始化操作,即 initState
function initState(vm) {
vm._watchers = []
var opts = vm.$options
if (opts.props) {
initProps(vm, opts.props)
}
if (opts.methods) {
initMethods(vm, opts.methods)
}
if (opts.data) {
initData(vm)
} else {
observe((vm._data = {}), true /* asRootData */)
}
if (opts.computed) {
initComputed(vm, opts.computed)
}
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
以 initData 为例,observe(data)
就是将 data 转化为响应式对象的入口,而在 observe 方法中,
- 首先判断如果传入的 value 不是对象,或者是 vnode 实例,则直接返回
- 然后根据对象的
__ob__
属性是否存在,判断对象是否已经是响应式的,是的话,就会直接返回该属性的值 - 最后就是实例化响应式对象的逻辑
new Observer(value)
function initData(vm) {
var data = vm.$options.data
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
// ...
// observe data
observe(data, true /* asRootData */)
}
function observe(value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
// ...
return ob
}
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
# Observer 类
这个类的主要工作,就是遍历对象或数组上的所有属性,并将其设置为响应式的
class Observer {
value: any
dep: Dep
vmCount: number // number of vms that have this object as root $data
constructor(value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 将 __ob__ 属性设置为 当前 observer 实例
def(value, '__ob__', this)
// 对数组的处理
if (Array.isArray(value)) {
// 如果当前环境支持 `__proto__` 属性
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
// 对对象的处理
this.walk(value)
}
}
// 遍历对象的所有自身属性,递归处理每个属性值
walk(obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 处理数组中的每一个元素
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
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
# 依赖收集
在 Observer 类中,对于对象和数组的处理是不一样的,对数组的处理相对会复杂一些,原因想必大家都已经很清楚了。现在就分开看一下
# 处理对象
对于对象的处理过程:利用 walk
方法,遍历对象自身所有可枚举属性(不包括符号属性),依次进行 defineReactive
处理。
其实所谓的响应式对象,就是通过 Object.defineProperty
方法,重新定义了对象属性的 getter 和 setter,其中:
getter
:使其能够在该值被读取的时候将依赖该数据的Watcher
实例添加到Dep
实例的subs
数组中去setter
:使得当该值被修改的时候,能够通过Dep
实例的notify
方法,通知所有相关的Watcher
实例完成更新操作
function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 对象的每个 key 都会对应一个 Dep 实例
const dep = new Dep()
// 获取属性描述符对象
const property = Object.getOwnPropertyDescriptor(obj, key)
// 如果属性不可配置,则跳过
if (property && property.configurable === false) {
return
}
// AOP 技巧
const getter = property && property.get
const setter = property && property.set
// 如果没有 getter,则通过 obj[key] 的方式获取属性值,并赋值给 val
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 递归创建响应式
let childOb = !shallow && 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) {
// ...
},
})
}
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
51
52
上面提到,在 getter
中会将与对象某个属性相关的 Watcher
实例添加到 Dep
依赖中,那要怎么才能知道究竟是哪个 Watcher
实例依赖了这个属性的值呢?这里用到的就是 Dep 类上的一个静态属性 target。还是以 App 组件为例,
- App 组件的渲染 Watcher 实例
class Watcher {
constructor() {
// ...
this.value = this.get()
}
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
// ...
} finally {
// ...
popTarget()
}
return value
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
当 App 组件 mount 过程中,会创建一个组件的
渲染 Watcher
实例,因此构造器中 get 方法会被触发,所以 get 方法内部就会通过pushTarget(this)
将当前 Watcher 实例挂到 Dep.target 上紧接着在获取 value 的过程中,this.getter 的执行,就会触发 App 组件的渲染函数 render。而 render 函数中,就会触发响应式数据的 getter,比如这里的
_vm.name
- App 组件的数据对象 data 以及 render 函数
{
data: function data() {
return {
name: 'App'
};
},
render: function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c("div", { staticClass: "content-wrapper" }, [
_vm._v(_vm._s(_vm.name))
])
},
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
由于此时 Dep.target 上已经存在值,所以就会将当前的 渲染 Watcher
实例添加进跟 _vm.name
这个属性绑定的 Dep 实例的 subs 数组中,这就完成了依赖收集的过程。
# 处理数组
访问对象属性,其取值与赋值操作,都能被 Object.defineProperty()
成功拦截,但是 Object.defineProperty()
在处理数组上存在如下问题:
对于数组变异方法
pop、push、shift、unshift、reverse、sort、splice
,Object.defineProperty()
无法监听到变更
针对这个问题,vue 是怎么做的呢?
答案:还是运用了 AOP 的编程思想,重写了 Array 原型上的数组变异方法
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
// 利用 AOP 技巧,注入自定义功能
Object.defineProperty(arrayMethods, method, {
value: function (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
},
enumerable: !!enumerable,
writable: true,
configurable: true,
})
})
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
# Object.defineProperty()
的缺陷
上述的问题可以通过 hack 的方式来解决,但还有一些问题是没有办法通过 hack 的方式解决的,
# 对于对象
Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
# 对于数组
Vue 不能检测以下数组的变动:
- 当利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当修改数组的长度时,例如:
vm.items.length = newLength
不过针对上面的问题,vue 官方文档也给了相应的解决方案 (opens new window),这里不细说了,只看下 Vue.set 方法的实现
# Vue.set(target, key, val)
function set(target: Array<any> | Object, key: any, val: any): any {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 派发更新
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// ...
},
set: function reactiveSetter(newVal) {
var value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) return
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 对新属性值进行响应式处理
childOb = !shallow && observe(newVal)
// 派发更新
dep.notify()
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
可以看到,派发更新的过程就是调用 dep.notify 的过程
// ======= Dep ======== //
class Dep {
static target: ?Watcher
constructor() {
this.id = uid++
this.subs = []
}
// ...
notify() {
// stabilize the subscriber list first
const subs = this.subs.slice()
// ...
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// ======= Watcher ======== //
class Watcher {
// ...
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
// 需要立即响应变更
this.run()
} else {
// 放到队列中等待下一次事件循环时,统一做出响应
queueWatcher(this)
}
}
}
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
Watcher 实例的 update 方法中,对不同的 watcher 实例有不同的处理,用户定义 watcher 时,如果不特别指定,lazy、sync 属性的值默认都为 false,所以一般都会走到 queueWatcher 的逻辑中去。 queueWatcher 的流程中,会涉及到 nextTick 相关的内容,下一篇再说,这篇就先到这里。