03依赖收集
响应式系统的依赖收集追踪原理
为什么需要依赖收集
例子,直接上代码:
new Vue({
template: `<div>
<span>{{text1}}</span>
<span>{{text2}}</span>
</div>`,
data: {
text1: 'text1',
text2: 'text2',
text3: 'text3',
}
})然后我们修改了text3的值 this.text3 = modify text3
我们修改了data中text3的数据,但是因为视图中并不需要用到text3,所以我们并不需要触发cb函数来更新视图,调用cb显然是不正确的。
再来一个例子:假设我们现在有一个全局对象,可能会在多个Vue对象中用到它进行展示
let globalObj = { text: 'global text' }
let o1 = new Vue({
template: `<div>
<span>{{text}}</span>
</div>`,
data: globalObj
})
let o2 = new Vue({
template: `<div>
<span>{{text}}</span>
</div>`,
data: globalObj
})然后,我们修改了全局对象 globalObj.text = 'global text vue'
我们应该通知o1、o2两个vm实例进行进行视图的更新,依赖收集会让text这个数据知道:"有两个地方依赖我(globalObj)的数据,当我变化时需要通知它们"。最终会形成数据与视图的一种对应关系,如下图:
<img src="https://user-gold-cdn.xitu.io/2018/1/5/160c4572fdd738f2?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
接下来我们介绍一下依赖收集是如何实现的。
订阅者 Dep
首先我们来实现一个订阅者Dep,它的主要作用是用来存放Watcher观察对象
class Dep {
constructor() {
// 用来存放Watcher对象的数组
this.subs = []
}
// 在subs中添加一个Watcher对象
addSub(sub) {
this.subs = push(sub)
}
// 通知所有Watcher对象更新视图
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}为了便于理解我们只实现了添加的部分代码,主要做了两件事情:
- 用
addSub方法可以在目前Dep对象中增加一个Watcher的订阅操作 - 用
notify方法通知目前Dep对象的subs中所有Watcher对象触发更新操作
观察者Watcher
class Watcher {
constructor() {
// 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到
Dep.target = this
}
update() {
// 更新视图的方法
}
}
Dep.target = null依赖收集
接下来我们修改一下 defineReactive以及Vue的构造函数,来完成依赖收集。
我们在闭包中增加了一个Dep类的对象,用于收集Watcher对象。在对象被读取时,会触发reactiveGetter函数 把当前的Watcher对象(存放在Dep.target中)收集到Dep类中去。之后如果当该对象被写入时,则会触发reactiveSetter方法,通知Dep类调用notify来触发所有Watcher对象的update方法更新对应视图。
function defineReactive(obj, key, val) {
// 一个Dep类
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 将Dep.target(即当前的Watcher对象)存入dep的subs中
dep.addSub(Dep.target)
return val
},
set: function reactiveSetter(newVal) {
if (newVal === val) return
// 在set时触发dep的notify来通知所有的watcher对象更新视图
dep.notify()
}
})
}
class Vue {
constructor(options) {
this._data = options.data
observer(this._data)
// 新建一个Watcher观察者对象,这时候Dep.target会指向这个target对象
new Watcher()
// 在这里模拟render过程,为了触发test属性的get函数
console.log('render', this._data.test)
}
}小结
依赖收集的过程:
首先在observer的过程中会注册get方法,该方法用来进行依赖收集。在它的闭包中会有一个Dep对象,这个对象用来存放Watcher对象的实例。
其实依赖收集的过程就是把Watcher实例存放到对应的Dep对象中去。get方法可以让当前的Watcher对象(Dep.target)存放到它的subs中(通过addSub方法)。
在数据变化时,set会调用Dep对象的notify方法通知它内部所有的Watcher对象进行视图更新。
这是Object.defineProperty的set/get方法处理的事情,那么依赖收集的前提条件有两个:
- 触发get方法
- 新建一个Watcher对象
这个我们在Vue的构造类中触发。新建一个Watcher对象只需要new出来,这时Dep.target已经指向了这个new出来的Watcher对象来。而触发get方法很简单,只要把render function进行渲染,那么其中的依赖收集的对象都会被读取,这里我们通过打印来模拟这一过程,读取test来触发get进行依赖收集。
本章介绍了依赖收集的过程,配合之前的响应式原理,已经把整个响应式系统介绍完毕。其主要就是get进行依赖收集。set通过观察者来个更新视图,配合上图仔细理解,一定能搞懂它!
