Vue2中-Dep 和 Watcher 之间的联系

241 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 32 天,点击查看活动详情

Vue2中-Dep 和 Watcher 之间的联系

start

  • 初次查看 Vue2 源码中 Dep 和 Watcher 相关的源码,发现它们互相存储,互相调用。
  • 第一次阅读是没有理解透彻它们之间的联系,今天再次阅读,豁然开朗,写一个文章记录一下。

正文

  1. 首先我们写一个基础的 Html 案例,代码如下:
<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>lazy_tomato</title>
  </head>

  <body>
    <div id="app">
      <div>{{ tomato }}</div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
    <script>
      new Vue({
        el: '#app',
        data() {
          return {
            tomato: '番茄',
          }
        },
      })
    </script>
  </body>
</html>
  1. 结合上述的案例,梳理一下代码执行逻辑:

    首先以 cdn 的方式引入了一个完整版 Vue.js,写了一个 Vue2 的基础案例。

    运行 JS 的时候,会执行 new Vue(),页面开始挂载 $mount

    $mount 中会执行 new Watcher() (即创建一个渲染 watcher,标记它为 w1)。

    然后在 new Watcher() 的过程中会依次执行下面的逻辑:

    • 首先在 Dep.target 上存储当前的 this (也就是 w1);

      Dep 是一个类,用 Dep.target 存储 w1 ,保证在全局环境同一时刻,Dep.target 上存储的是 w1。(方便其他函数获取);

    • 随后执行 updateComponent ,这个函数会 创建虚拟DOM + 渲染页面。由于页面中使用了 data 中的 tomato 属性,随即会触发 tomatogetter
    • updateComponent 执行完毕后,然后会调用 w1.cleanupDeps 方法;(这个方法后续会讲)

    上述逻辑代码如下:

    // 精简过后的代码
    let updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
    
    new Watcher(vm, updateComponent, noop, true)
    
  2. 理解一下触发属性 tomatogetter 逻辑:

    对于属性 tomato,会有一个专门收集它相关依赖的 dep (暂记为d1)。

    上述的 dep 是闭包中定义的 dep。

    当触发属性 tomatogetter,就会触发 dep.depend (也就是d1.depend)。

    tomato 的 getter

    var dep = new Dep();
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
          if (Dep.target) {
            dep.depend();
          }
          return value;
        },
      }
    

    dep.depend 函数中会触发 Dep.target.addDep(this);Dep.target 上存储的是当前 Watcher实例 ( w1 ),所以等同于 w1.addDep(d1) (此时的 this 指向当前的 dep,也就是 d1

    dep.depend

    Dep.prototype.depend = function depend() {
      if (Dep.target) {
        Dep.target.addDep(this)
        // Dep.target.addDep(this) 可以理解为  `w1.addDep(dep)`
      }
    }
    
  3. 在看 w1.addDep(d1) 的具体逻辑之前,先介绍一下 Watcher实例(w1) 上的四个变量:

    • deps = []; 存储旧 dep 的数组
    • depIds = new Set(); 存储去重后的 旧 dep 的 id
    • newDeps = []; 存储新 dep 的数组
    • newDepIds = new Set(); 去重 新 dep 容器

    deps , depIds 存储旧 dep 的相关信息;newDeps , newDepIds存储新 dep 的相关信息。

    新旧 dep 是什么概念? 当页面发生改变触发 getter,旧dep 存储上次的依赖; 新dep 存储最新的依赖。

    其中数组类型的变量(deps , newDeps)是用来存储真实的 dep 对象

    Set 类型的变量(depIds , newDepIds)是用来存储去重后的 dep 的 id。 (使用 Set 类型的数据目的是为了去重)

    首次渲染的时候,旧dep 相关的变量为空。

  4. w1.addDep的逻辑:

    如果 w1.newDepIds 中没有存储 d1的idw1的新dep 中存储 d1。如果w1的新旧dep 都没有 d1的idd1 存储当前的 Watcher实例 ( w1 )。

    这个地方可能有点绕,对照下方的代码,我解释一下:

    1. 逻辑的起点是 dep.depend d1.depend

    2. Dep.target 上存储的当前 Watcher实例 存储 dep w1上存储d1

    3. 如果 Watcher实例 从来就没有存储过我这个 dep, 我这个 dep 存储一下当前的 Watcher实例 d1上存储w1

    这个逻辑过滤下来就实现了:Watcher实例 中存储 Dep实例Dep实例 中存储了 Watcher实例

    w1.addDep

    Watcher.prototype.addDep = function addDep(dep) {
      // dep => d1
      // this => w1
      var id = dep.id
      if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id)
        this.newDeps.push(dep)
        if (!this.depIds.has(id)) {
          dep.addSub(this)
        }
      }
    }
    
  5. 再说说 cleanupDeps

    Watcher实例 上,如果旧dep存在新dep不存在。(说明之前有依赖过,最新的没有依赖,1.自身不会存储旧的 dep;2.通知对应 dep 删除我这个 Watcher 实例)

    Watcher.prototype.cleanupDeps = function cleanupDeps() {
      var i = this.deps.length
    
      // 遍历旧的dep,如果新的dep中没有对应id的dep,说明旧的dep项不需要依赖了, 旧的dep项删除我这个watcher
      while (i--) {
        var dep = this.deps[i]
        if (!this.newDepIds.has(dep.id)) {
          dep.removeSub(this)
        }
      }
    
      // 这一次逻辑执行完毕了,更新旧的dep为此次手机的最新的dep, 新dep数组清空,供下次使用。
      var tmp = this.depIds
      this.depIds = this.newDepIds
      this.newDepIds = tmp
      this.newDepIds.clear()
      tmp = this.deps
      this.deps = this.newDeps
      this.newDeps = tmp
      this.newDeps.length = 0
    }
    

提问

1.dep 中存储的 watcher 有什么用?

当我的数据改变的时候,也就是触发了数据setter 的时候。遍历 dep 中存储的每一个 wathcer实例 ,并调用 watcher实例 上的 update方法通知更新;

2.watcher 中存储 dep 有什么用?

  1. 去重,可以避免重复的向 dep 中 push 同一个 watcher。(对应函数 Watcher.prototype.addDep ;当然去重的逻辑也可以放在 Dep 中处理)
  2. 可以对无用的 dep 进行清除,可以对 dep 中无用的 watcher 进行清除。 (对应函数 cleanupDeps;就比如上次渲染有一个 watcher 观察了一个数据,最新的页面,没有关注这个数据了,1.Watcher 中的newDepIds,newDeps不收集旧的 dep;2.通知 dep 中去除当前的 watcher)
  3. 当页面销毁的时候,告诉所有存储了我这个 watcher实例 的 dep,删除我这个 watcher。(对应函数 Vue.prototype.$destroy ,组件销毁的时候,会遍历组件上的 _watcher 属性,然后调用 Watcher.prototype.teardown方法,遍历 watcher.deps, 删除对应 dep 中对应的 watcher实例

Watcher 上的 _watcher 属性

上面提到了 Watcher 上的 _watcher属性,介绍一下它。

代码:

var Watcher = function Watcher (
  vm,
  expOrFn,
  cb,
  options,
  isRenderWatcher
) {
  this.vm = vm;
  if (isRenderWatcher) {
    vm._watcher = this;
  }
  vm._watchers.push(this);
}

每个组件的上都有 _watcher 属性,它存储的是这个组件中所有的 watcher实例。例如渲染watcher,计算属性,用户自定义的watcher。

这个属性的作用就是,在组件销毁的时候,找到这个组件的所有 watcher实例,告诉对应的 dep,取消对对应watcher实例的收集。

end

好啦,用这么多篇幅,介绍了 Dep 和 Watcher 之间的关系。它们两个的实例互相存储,归根到底是为了满足各自的任务。分工明确,既能保证我存储了你,方便能随时找到你,和你说话。又能保证你销毁的时候,你会主动告诉我,你快被销毁了。例子中:我就是dep,你就是 watcher