Skip to content

03依赖收集

响应式系统的依赖收集追踪原理

为什么需要依赖收集

例子,直接上代码:

js
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对象中用到它进行展示

js
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观察对象

js
class Dep {
  constructor() {
    // 用来存放Watcher对象的数组
    this.subs = []
  }
  // 在subs中添加一个Watcher对象
  addSub(sub) {
    this.subs = push(sub)
  }
  // 通知所有Watcher对象更新视图
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

为了便于理解我们只实现了添加的部分代码,主要做了两件事情:

  1. addSub方法可以在目前Dep对象中增加一个Watcher的订阅操作
  2. notify方法通知目前Dep对象的subs中所有Watcher对象触发更新操作

观察者Watcher

js
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方法更新对应视图。

js
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.definePropertyset/get方法处理的事情,那么依赖收集的前提条件有两个:

  1. 触发get方法
  2. 新建一个Watcher对象

这个我们在Vue的构造类中触发。新建一个Watcher对象只需要new出来,这时Dep.target已经指向了这个new出来的Watcher对象来。而触发get方法很简单,只要把render function进行渲染,那么其中的依赖收集的对象都会被读取,这里我们通过打印来模拟这一过程,读取test来触发get进行依赖收集。

<img src="https://user-gold-cdn.xitu.io/2017/12/19/1606edad5ca9e23d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">

本章介绍了依赖收集的过程,配合之前的响应式原理,已经把整个响应式系统介绍完毕。其主要就是get进行依赖收集。set通过观察者来个更新视图,配合上图仔细理解,一定能搞懂它!

共 20 个模块,1301 篇 Markdown 文档。