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

699 阅读4分钟

为什么要依赖收集?

先举个例子:

我们现在有这么一个 Vue 对象。

  new Vue({
    template: `<div>
        <span>{{text1}}</span>
        <span>{{text2}}</span>
      </div>`,
    data: {
      text1: 'text1',
      text2: 'text2',
      text3: 'text3',
    }
  })

然后我们做了这么一个操作。

  this.text3 = "modify text3";

我们修改了 datatext3 的数据,但是因为视图中并不需要用到 text3, 所以我们并不需要触发上一章所讲的 cb 函数来更新视图,调用 cb 显然是不正确的。

再来一个例子:

假设我们现在有一个全局的对象,我们可能会在多个 Vue 对象中用到它进行展示。

  let globalObj = {
    text1: 'text1',
  };

  let o1 = new Vue({
    template: `
      <div>
        <span>{{text1}}</span>
      </div>
    `,
    data: globalObj
  });

  let o2 = new Vue({
    template: `<div>
      <span>{{text1}}</span>
    </div>`,
    data: globalObj
  });

这个时候,我们执行了如下操作。

  global.text1 = 'hello,text1';

我们应该需要通知 o1 以及 o2 两个 vm 实例进行视图的更新。 【依赖收集】会让 text1 这个数据知道“哦~有两个地方依赖我的数据,我变化的时候需要通知它们~”。

最终会形成数据与视图的一种对应关系,如下图。

数据与视图对应关系

接下来我们来介绍一下【依赖收集】是如何实现的。

订阅者 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 () {
      console.log("视图更新啦~")
    }
  }

  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观察者对象调用自身 update 方法来更新视图
        dep.notify();
      }
    })
  }

  class Vue {
    constructor (options) {
      this._data = options.data;
      observer(this._data); // 一般这个接下来会做一个Object,keys(data) 来递归调用defineReactive方法
      // 新建一个watcher观察者对象,这时候dep.target会指向这个watcher对象
      new Watcher();

      // 在这里模拟render的过程,为了触发test属性的get函数
      console.log(`render~: ${this._data.test}`);
    }
  }

小结

总结一下。

首先在 observer 的过程中会注册 get 方法,该方法用来进行 【依赖收集】(dep.addSub(Dep.target))。在它的闭包中会有一个 Dep 对象, 这个对象用来存放 Watcher 对象的实例。 其实 【依赖收集】的过程就是把 Watcher 实例存放到对应的 Dep 对象中去。 get 方法可以让当前 watcher 对象(Dep.target)存放到它的 subs 中 (addSub)方法,在数据变化时,set 会调用 Dep 对象的 notify 方法 通知它内部所有的 watcher 对象用自身 update 方法进行视图更新。 (vm._update(vm._render())

这是 Object.definePropertyset/get 方法处理的事情,那么【依赖收集】的前提条件还有两个:

  • 触发 get 方法;
  • 新建一个 Watcher 对象;

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

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

响应式系统

注:本节代码参考 《响应式系统的依赖收集追踪原理》