美文网首页
从源码的角度分析vue computed的依赖搜集

从源码的角度分析vue computed的依赖搜集

作者: hello_小丁同学 | 来源:发表于2020-12-06 15:06 被阅读0次

vue 源码版本是2.6.12

缘起

很多介绍vue源码的文章对computed怎么计算值讲的很清楚,但是对computed 怎么搜集到依赖它的视图渲染watcher,以及怎么去通知对应的渲染watcher去更新讲解的很模糊或者干脆一笔带过。这篇文章主要讲解——computed watcher是怎么搜集到订阅它的渲染watcher。

<template>
    <div>   
        <div>{{a}}</div>
    </div>
</template>
<script>
export default {
    data() {
        return {
            i: 0
        }
    }, 
    computed: {
        a() {
            return this.i
        }
    },
}

依赖搜集

文件在src/core/instance/state.js

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

当组件读取computed a的值的时候会执行 computedGetter函数,先是通过

      if (watcher.dirty) {
        watcher.evaluate()
      }

计算出computed函数的值,然后通过

      if (Dep.target) {
        watcher.depend()
      }

进行依赖搜集。
Dep.target指向当前组件的渲染watcher,进入watcher.depend()看看是怎么进行依赖搜集的
文件位于 src/core/observer/watcher.js

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

第一个问题:this.deps的赋值

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let 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
  }

是在cleanupDeps函数中执行this.deps = this.newDeps,所以要看cleanupDeps在哪里被调用的,以及this.newDeps中的值是哪里产生的

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

get函数是在computed 通过watcher.evaluate()计算值的时候被调用的,讲解下这个函数的核心操作

1. pushTarget(this)

这个this是计算属性的watcher,调用dep.js中的

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

作用是放到栈顶,同时将计算属性的watcher赋值给Dep.taget

2. value = this.getter.call(vm, vm)

会调用 计算属性a的函数

     {
            return this.i
        }

由于引用到了i,所以会触发i的get 函数,就会调用dep.depend(),实际上是i的依赖搜集,这里的dep对象属于i

    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },

dep.depend() 位于src/core/observer/dep.js

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

这里的Dep.target就是上面保存的computed watcher实例,会执行watcher中的addDep,这里的this就是i的dep实例
文件位于 src/core/observer/watcher.js

  /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

做了两件事

  1. 让i的dep持有a 的watcher实例,完成依赖搜集
  2. 将i的dep实例存到this.newDeps
3.popTarget()
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

把栈顶的watcher弹出,改变Dep.target的指向,此时指向组件的渲染watcher

4.cleanupDeps
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let 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
  }

这一步就是 将this.newDeps的值赋给this.deps,此时this.deps中的数组中的对象其实就是i的dep实例

再回到 watcher.depend()

  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

this.deps[i].depend() 这里就是执行

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

此时Dep.target是组件的渲染watcher,所以实现的逻辑是组件渲染watcher调用addDep(this),其实就是持有i的dep,最终被i搜集到依赖。
转了这么大一圈,实际上是为了让组件的watcher被计算属性中引用的data变量搜集到,这也不难理解,既然组件依赖computed的变化,当然也依赖computed中的值的变化,示例中computed中的值变化来自于i的变化,所以当i变化时,就让去通知计算属性的watcher去重新计算,通知组件watcher重新渲染。
对于data中变量的响应式原理和依赖搜集、派发更新可以参考我的这篇文章
从源码的角度分析Vue视图更新和nexttick机制

参考:
https://ustbhuangyi.github.io/vue-analysis/v2/reactive/getters.html#dep
https://juejin.cn/post/6877451301618352141

相关文章

网友评论

      本文标题:从源码的角度分析vue computed的依赖搜集

      本文链接:https://www.haomeiwen.com/subject/ioyowktx.html