原文地址 - 从零开始 - Vue 响应式原理白话
简单来说,依赖收集的过程是:
init
的过程中,将data
中的属性添加getter/setter
方法render
函数执行时),每个组件实例内部会实例化一个Watcher
对象,data
中的属性会被touch
,触发getter
方法,记录组件和属性的对应关系setter
方法,会调用对应的wachter
重新计算,调用render
函数,导致关联组件更新data
的getter/setter
在init
阶段,data
中的属性会被添加getter
和setter
方法,手段就是调用Object.defineProperty
方法
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
:
mountComponent(vm: Component, el: ? Element, ...) {
vm.$el = el
...
updateComponent = () = > {
vm._update(vm._render(), ...)
}
new Watcher(vm, updateComponent, ...)
...
}
在组件渲染过程中,实例化了一个Watcher
对象,它有两个参数,第一个参数vm
就是当前组件实例,第二个参数updateComponent
会调用vm._render
和vm._update
方法,主要目的就是更新页面
vm._render
目的是将Vue对象渲染为虚拟DOM
``vm._update`目的是将虚拟DOM创建或更新为真实DOM
在组件需要更新的时候,Watcher
就会被调用,更新页面
lass 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
对象的构造函数中看:
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)
也就是调用了Watcher
的addDep
方法
而Watcher
中的addDep
方法简化后:
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
记录到了组件的Dep
的subs
中
修改data
中已经双向绑定后的属性,会调用setter
方法,调用了dep.notify
方法
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
-Watche
r-组件
是一一对应的,data中的每个属性都有对应的dep
实例
更详细的细节:
由于在Vue的init
过程中,对data
中的属性执行了getter
和setter
转化过程,如果是未初始化话添加的data
根属性,则无法被追踪:
var vm = new Vue({
data: {
a: 1
}
})
// `vm.a` 是响应的
vm.b = 2
// `vm.b` 是非响应的
还有一个问题:
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的
items
是数组,数组的索引并不是属性,很难用Dep
去绑定,长度length
也没有被处理,解决方法:
vm.items.splice(indexOfItem, 1, newValue)
vm.items.splice(newLength)