Vue源码01 Vue响应式原理

Vue响应式原理

响应式过程

简单来说,依赖收集的过程是:

  1. 在组件init的过程中,为data中的属性添加getter/setter方法
  2. 在组件渲染过程中(render函数执行时),每个组件实例内部会实例化一个Watcher对象,data中的属性会被touch,触发getter方法,记录组件和属性的对应关系
  3. 当属性更新时,访问setter方法,会调用对应的wachter重新计算,调用render函数,导致关联组件更新

datagetter/setter

init阶段,data中的属性会被添加gettersetter方法,手段就是调用Object.defineProperty方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function defineReactive(obj: Object, key: string, ...) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {....
dep.depend()
return value....
},
set: function reactiveSetter(newVal) {...
val = newVal
dep.notify()...
}
})
}

data中的每个属性都有一个dep对象,getter中会调用dep.depend()方法

Watcher的创建

Vue对象在init之后进入mount阶段,关键函数是mountComponent

1
2
3
4
5
6
7
8
9
10
11
12
mountComponent(vm: Component, el: ? Element, ...) {
vm.$el = el

...

updateComponent = () = > {
vm._update(vm._render(), ...)
}

new Watcher(vm, updateComponent, ...)
...
}

在组件渲染过程中,实例化了一个Watcher对象,它有两个参数,第一个参数vm就是当前组件实例,第二个参数updateComponent会调用vm._rendervm._update方法,主要目的就是更新页面

vm._render目的是将Vue对象渲染为虚拟DOM
vm._update目的是将虚拟DOM创建或更新为真实DOM

在组件需要更新的时候,Watcher就会被调用,更新页面

收集依赖

1
2
3
4
5
6
7
8
9
10
11
12
class Watcher {
getter: Function;

// 代码经过简化
constructor(vm: Component, expOrFn: string | Function, ...) {
...
this.getter = expOrFn
Dep.target = this // 暂且不管
this.value = this.getter.call(vm, vm) // 调用组件的更新函数
...
}
}

Watcher的构造函数中,会调用expOrFn方法,这个expOrFn就是上面的updateComponent方法,进而调用vm._render方法

在这个过程中,会访问data中的属性的getter(也就是touch的过程),这是会调用上面提到的dep.depend()方法

为了弄清dep.depend()方法究竟做了什么,在dep对象的构造函数中看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Dep {
static target: ? Watcher;
subs : Array < Watcher > ;

depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}

notify() {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

dep.depend()实际上调用了Dep.target.addDep(this),而在Watcher的构造函数中有这样的代码Dep.target = this,所以Dep.target.addDep(this)也就是调用了WatcheraddDep方法

Watcher中的addDep方法简化后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Watcher {
addDep(dep: Dep) {
...
this.newDeps.push(dep)
dep.addSub(this)
...
}
}

class Dep {
addSub(sub: Watcher) {
this.subs.push(sub)
}
}

Watcher将这个Dep保存了下来,然后调用了Dep的addSub方法,将Watcher存了进去

我这样理解,Dep记录了所有依赖这个属性的组件和组件的Watcher实例,同样,在Watcher中也记录了当前组件实例都使用了哪些属性

经过这些步骤,Dep.depend的导致addSub方法被调用,将当前的Watcher记录到了组件的Depsubs

派发更新

修改data中已经双向绑定后的属性,会调用setter方法,调用了dep.notify方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Dep {
static target: ? Watcher;
subs : Array < Watcher > ;

depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}

notify() {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

dep.notify方法调用所有依赖这个属性的组件的Watcher实例,运行render方法,更新页面

render-Watcher-组件是一一对应的,data中的每个属性都有对应的dep实例

更详细的细节:

问题

由于在Vue的init过程中,对data中的属性执行了gettersetter转化过程,如果是未初始化话添加的data根属性,则无法被追踪:

1
2
3
4
5
6
7
8
9
10
var vm = new Vue({
data: {
a: 1
}
})

// `vm.a` 是响应的

vm.b = 2
// `vm.b` 是非响应的

还有一个问题:

1
2
3
4
5
6
7
8
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})

vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

items是数组,数组的索引并不是属性,很难用Dep去绑定,长度length也没有被处理,解决方法:

1
2
vm.items.splice(indexOfItem, 1, newValue)
vm.items.splice(newLength)

参考