阅读 1513

[VUE]computed属性的数据响应和依赖缓存实现过程

本文是一篇vue.js源码学习笔记,适合对vue的数据响应实现有一定了解的同学,文中有表述不准确的地方还望指出。那么开始了

提示:文中会看到( 知识点:xxx )这样的标记表示在源码的相应位置打上标记, 方便大家阅读

一,先看computed的实现源码

/src/core/instance/state.js

// initState会在new Vue()时执行
export function initState (vm: Component) {
  /*
  	other
  */
  // 如果我们定义了comouted属性则执行initComputed
  if (opts.computed) initComputed(vm, opts.computed)
  /*
  	other
  */
}
复制代码

在同一个文件中找到initComputed的定义

function initComputed (vm, computed) {
  // 往组件实例上添加一个_computedWatchers属性,保存所有的computed watcher
  const watchers = vm._computedWatchers = Object.create(null)
  
  // 对所有的computed属性遍历处理
  for (const key in computed) {
  
    // 将我们定义的computed属性值用userDef保存
    const userDef = computed[key]
    
    // 我们在定义computed时可以是一个函数,也可以是一个对象{get:function(){}, set:function(){}}
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    
    // 数据响应过程中的watcher(注意第二个参数是我们刚才拿到的getter,记住了)
    watchers[key] = new Watcher(
      vm,
      getter || noop, // 注意这里,注意这里,注意这里,(****知识点getter)
      noop,
      computedWatcherOptions
    )
    
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } 
  }
}
复制代码

接下来我们还是在这个文件中找到defineComputed的实现

export function defineComputed (target, key, userDef) {
  /* other */

  // 这里我对源码进行了简化
  // sharedPropertyDefinition是一个全局对象
  // 拿到一个get函数
  sharedPropertyDefinition.get = createComputedGetter(key)

  /* other */

  // 这个函数的主要功能是computed属性的get进行了重写
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
复制代码

还是继续看createComputedGetter很重要,很重要,很重要

function createComputedGetter (key) {

  // 返回一个函数,也就是我们在上一个函数中那个get函数
  return function computedGetter () {

    // 拿到我们在initComputed函数中添加到vm上面的_computedWatchers
    const watcher = this._computedWatchers && this._computedWatchers[key]

    // 如果我们有定义computed属性,watcher必定存在
    if (watcher) {
      
      // 注意,注意,注意,只要在模板中使用了这个computed属性,之后每次页面更新就是循环知识点1到知识点5这个过程
      // 第二节主要就是在讲这一块,在理解下面的步骤时可以对照的看一下
      if (watcher.dirty) { // ****标记:知识点1
        watcher.evaluate() // ****标记:知识点2
      }
      if (Dep.target) { // ****标记:知识点3
        watcher.depend() // ****标记:知识点4
      }
      return watcher.value // ****标记:知识点5
    }
  }
}
复制代码

二,computed的属性响应

1,一切的起源

还是在这个文件中我们可以找到这个对象

const computedWatcherOptions = { lazy: true }
复制代码

回到initComputed函数中new Watcher时

watchers[key] = new Watcher(
  vm,
  getter || noop, 
  noop,
  computedWatcherOptions  // 看这里,看这里,看这里(传入{lazy: true})
)
复制代码
2,一切的开始

Watcher的源码

constructor (vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    /* ... */
    if (options) {
      this.lazy = !!options.lazy // this.lazy = true
    }
    this.getter = expOrFn
    this.dirty = this.lazy // 初始化this.dirty = true
    /* ... */
    // 注意了,注意了,注意了
    // new时this.lazy为true所以this.value = 'undefined'
    this.value = this.lazy ? undefined : this.get()
  }
复制代码

主要流程在createComputedGetter函数里面

  • new Watcherthis.dirty = this.lazy 所以知识点1:watcher.dirty = true
  • 接着知识点2: watcher.evaluate()会将this.dirty = false 接着会执行Watcher的this.get()最终其实就是执行知识点getter:this.getter()
  • 继续知识点3:Dep.target永远为真
  • 知识点4收集依赖
  • 最后知识点5获得computed的值

对Watcher的实现这里就说这么多了,本文是建立在大家对Wathcer类有一定了解的基础上讲解的。如果大家有需要可以留言后期会给大家详细梳理下数据响应的实现过程,网上好像已经有很多相关的文章了

3,开始表演
new Vue({
  data(){
    return {
      dataA: 'a',
      dataB: 'b'
    }
  },
  template: '<div>{{computedA}}-{{dataB}}</div>',
  computed: {
    computedA() {
      return 'computed ' + this.dataA
    }
  },
  method: {
      changeA(){
          this.dataA = 'change dataA'
      },
      changeB(){
          this.dataA = 'change dataB'
      }
  }
})
复制代码

看在createComputedGetter函数

  • 1,第一次页面渲染时模板中的{{computedA}}执行computedA.get() 转到函数createComputedGetter中
  • 2,知识点1: this.dirty = true
  • 3,知识点2:watcher.evaluate()执行,将this.dirty = false,watcher内部执行知识点getter:this.getter()
this.getter = computedA = function(){
    return 'computed' + this.dataA // 看这里,看这里,看这里,知识点update
}
复制代码

得到了wacher.value 等于 computed a

  • 4,watcher.depend()重新收集依赖
  • 5,返回wacher.value,渲染到页面上<div>computed a-b</div>
  • 6,我们通过调用this.changA()改变dataA,调用dataA中的dep.notify(),会执行dataA的所有watcher对象wathcer.update(),因为computed所属watcher的lazy永远为true知识点1: this.dirty = true
  • 7,因为dataA改变了,触发页面重新渲染,重新渲染模板,模板中的{{computedA}}再次调用computedA.get(),循环第1步

三,computed的属性缓存

通过第二节3段中computed的响应过程我们知道,computedA会监听dataA的改变去改变知识点1: this.dirty = true才最终执行了知识点getter

假设:

  • 我们现在执行this.changeB(),改变了dataB值,
  • 会执行dataB的所有watcher对象wathcer.update()
  • 因为dataB改变了,触发页面重新渲染,重新渲染模板,模板中的{{computedA}}再次调用computedA.get()转到函数createComputedGetter中
  • 因为computedA并没有监听dataB的改变,也就不会执行到computedAwatcher.update()知识点1:this.dirty = false,最终 并不会执行到知识点:getter
  • 直接返回上次知识点:getter的结果return watcher.value

总结

因为这篇主要是讲computed的知识,对于数据响应的知识没有详细说明,可能看起来有点懵逼,后面有需要会详细分析下vue数据响应的实现过程。

关注下面的标签,发现更多相似文章
评论