Vue2 computed 原理及源码分析 为什么 watcher 会有维护两个 dep数组

81 阅读6分钟

Vue computed 大家都知道有缓存机制,那他是如何实现的,并且如何收集依赖的呢? 一起来看看吧~

Vue 相关

Vue 双向绑定原理

讲的不完美,很多确缺点,主要部分会用特殊颜色标注!

  1. 了解 computed 的基本用法
let data = function(){
    return {
        data1: 1,
    }
};
let computed = {
    computed1: function(){
        // 不可以用箭头函数 否则 this 不是指向 vue 实例
        return this.data1 * 1
    },
    computed2: {
        get(){
         return this.data1
        },
        set(newVal,oldVal){
            this.data1 = newVal
        }
    }
};
  1. 主要功能讲解,此阶段时初始化阶段,不是访问 computed 属性阶段
    1. 在 init computed 属性时,首先会在 vm 绑定 computedWatchers 属性,为一个空对象
    2. 接下来 会判断属性是否是'function',且获取 computed 的 get属性
    3. 为上面创建的 computedWatchers 对象添加属性 key 为 computed 中的key,值为 computedWatcher(及new Watcher)
    4. new Watcher 是,把第3步获取的 get 属性作为 watcher 的 getter,并标记此 watcher 是 lazy,表示computedWatcher
    5. 重构 computed 的 get,让 get 通过 第3步创建的 computedWatcher 来收集依赖和缓存value
【重构get函数源码】(点击展开) ```js
    function createComputedGetter(key) {
      return function computedGetter() {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          if (watcher.dirty) {
            watcher.evaluate() // 执行完后 Dep.targer 不是当前 computedWatcher
          }
          if (Dep.target) { // 有值的清空是比如有另一个 computed 或者 渲染watcher 访问当当前的 computed 如: computed 的基本用法中 computed2 的例子
            watcher.depend()
          }
          return watcher.value 
        }
      }
    }

```

访问 computed 属性时 发生了什么?

  1. 在 get 执行时,会先获取当前 compute 的 watcher
  2. 判断 watcher 是否是脏的,也就是依赖有没有变化
  3. 如果是脏数据,就让当前的 watcher 执行 get
  4. watcher 执行 get 就会把当前的 computedWatcher 赋值给全局 Dep.target ,然后执行 computed【key】的 get
  5. 在执行computed get 时, 就会访问到 数据劫持过的 data 数据,就会把访问的 data 属性 添加到 全局的 Dep.target(computedWatcher)依赖中
  6. 然后 watcher 在 get 最后还会执行 cleanupDeps,就是清理旧的依赖 '(会维护两个 deps 数组)'
  7. 把全局的 Dep.target 退回到上一个 watcher
  8. watcher 执行完 get 后 把当前的 'wathcer.dirty 标记为 false' ,判断 全局的 Dep.target 是否有值, 如果有值,就执行 watcher.depend 来完成 当前 computedWatcher 所依赖的 dep 【注意1:此时收集 data属性中 dep 的 watcher 不是 当前访问的 computedWatcher,原理下方有说明
  9. 返回 getter 执行完得到的 value 并返回 value

computed 的缓存主要重写了 get 属性,并为 computed[key] 创建了 watcher 实例(computedWatcher),看 computedWatcher 是否是脏的这个标记, 如果是脏的(表示依赖被修改过),就重新执行 computedWatcher 的 get 否则直接返回 computedWatcher.value

注意的解释

注意1: 在 computed get 执行完后, 判断当前的全局 Dep.target 是否有值 有值才收集 computedWatcher 的依赖的原因:

  1. 访问 computed 的依赖项可能没有被 其他依赖所依赖, 比如 computed1 中依赖了 computed2 属性, 那 computed1 没有收集到 computed2 所依赖的 dep 当 computed2 所依赖的 属性改变时, 会通知不到
  2. 访问 computed 的依赖项可能没有被 渲染watcher 所依赖, 因为不是所有的 data 属性都会用在渲染模板里面被访问

Watcher对象维护两个与依赖(Dep)相关的数组:depsnewDeps。这两个数组服务于不同的目的,主要是为了在Vue实例生命周期的不同阶段(尤其是组件更新过程)有效地管理依赖关系。比较官方的说明它们各自的职责和使用场景:

deps数组:

  • 作用:存储当前Watcher实例在初始化或执行时收集到的所有依赖(Dep对象)。这些依赖通常是组件模板中使用的数据绑定表达式所对应的依赖。当这些依赖(即数据源)发生变化时,Dep对象会通知其所有者Watcher进行更新。
  • 生命周期deps数组主要在组件渲染期间发挥作用,即在创建Watcher实例时(首次渲染或组件更新时)通过遍历模板并访问数据属性触发依赖收集,将对应的Dep对象添加到deps数组中。在组件的整个生命周期内,deps数组通常保持不变,除非组件再次进入更新流程。
  • 通知更新:当Dep对象内部的数据发生变化时,会触发其notify方法,遍历并调用deps数组中所有Watcher实例的update方法,启动Vue的更新流程。

newDeps数组:

  • 作用:在组件进行更新时,新的依赖收集阶段临时存放新收集到的依赖(Dep对象)。Vue在进行组件更新时,会重新遍历模板并访问数据属性,这个过程会触发新的依赖收集。新收集到的依赖被暂时保存在newDeps数组中。
  • 生命周期newDeps数组仅在组件更新的特定阶段(即依赖重新收集阶段)存在,用于记录本次更新中新产生的依赖关系。在依赖收集阶段结束后,Vue会比较depsnewDeps,执行必要的依赖关系管理操作(如移除已不存在的依赖、添加新依赖),然后清空newDeps数组。
  • 依赖关系管理:比较depsnewDeps的目的在于处理依赖关系的变化。如果某个旧依赖(在deps中)在新收集阶段未出现(不在newDeps中),说明该依赖不再被当前Watcher所关注,应将其从deps中移除,释放相关资源。反之,如果newDeps中存在deps中没有的依赖,则将其添加到deps中,确保Watcher能够接收到新依赖的变化通知。

综上所述,Watcher维护两个dep数组的原因在于:

  • deps数组用于持久存储当前Watcher在正常运行时需要监听的稳定依赖关系,确保在数据变化时能及时得到通知并触发相应的更新流程。
  • newDeps数组则是在组件更新过程中临时存放新收集到的依赖关系,用于对比和管理依赖关系的变化,确保Watcher依赖列表的准确性和有效性,避免无效监听和资源浪费。在依赖关系处理完毕后,newDeps数组会被清空,等待下一次更新时重新使用。

通俗一点就是,在 computed get 中 如果有 if 判断 就可能会有两种及以上的不同依赖。 所以在首次 computed 收集依赖时, 收集初始化的依赖放入有 deps 中,在 get 执行完毕之后,清空 newDeps 以下次收集新的依赖,在下次收集依赖时,对比新旧依赖(deps 和 newDeps),确保 deps 的依赖项是最新的,避免无效监听和资源浪费。