美文网首页
从源码的角度分析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