Vue中computed的本质—lazy Watch

两个月前我曾在掘金翻译了一篇关于Vue中简单介绍computed是如何工做的文章,翻译的很通常因此我就不贴地址了。有位我很是敬佩的前辈对文章作了评价,内容就是本文的标题“感受原文并无讲清楚 computed 实现的本质- lazy watcher”。上周末正好研究一下Vue的源码,特地看了computed,把本身看的成果和你们分享出来。javascript

Tips:若是你以前没有看过Vue的源码或者不太了解Vue数据绑定的原理的话,推荐你看我以前的一篇文章简单易懂的Vue数据绑定源码解读,或者其余论坛博客相关的文章均可以(这种文章网上很是多)。由于要看懂这篇文章,是须要这个知识点的。html

一. initComputed 

首先,先假设传入这样的一组computedvue

//先假设有两个data: data_one 和 data_two
computed:{
    isComputed:function(){
        return this.data_one + 1;
    },
    isMethods:function(){
        return this.data_two + this.data_one;
    }
}
复制代码

咱们知道,在new Vue()的时候会作一系列初始化的操做,Vue中的data,props,methods,computed都是在这里初始化的:java

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) //初始化props
  if (opts.methods) initMethods(vm, opts.methods) //初始化methods
  if (opts.data) {
    initData(vm) //初始化data
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed) //初始化computed
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch) //初始化initWatch
  }
}
复制代码

我在数据绑定的那边文章里,详细介绍了initData()这个函数,而这篇文章,我则重点深刻initComputed()这个函数。缓存

const computedWatcherOptions = { lazy: true } //用于传入Watcher实例的一个对象

function initComputed (vm: Component, computed: Object) {
  //声明一个watchers,同时挂载到Vue实例上
  const watchers = vm._computedWatchers = Object.create(null)
  //是不是服务器渲染
  const isSSR = isServerRendering()

  //遍历传入的computed
  for (const key in computed) {
    //userDef是computed对象中的每个方法
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }
    
    //若是不是服务端渲染的,就建立一个Watcher实例
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    if (!(key in vm)) {
      //若是computed中的key没有在vm中,经过defineComputed挂载上去
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      //后面都是警告computed中的key重名的
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
复制代码

initComputed以前,咱们看到声明了一个computedWatcherOptions的对象,这个对象是实现"lazy Watcher"的关键。bash

接下来看initComputed,它先声明了一个名为watchers的空对象,同时在vm上也挂载了这个空对象。以后遍历计算属性,并把每一个属性的方法赋给userDef,若是userDef是function的话就赋给getter,接着判断是不是服务端渲染,若是不是的话就建立一个Watcher实例。Watcher实例我也在上一篇文章分析过,就不逐行分析了,不过须要注意的是,这里新建的实例中咱们传入了第四个参数,也就是computedWatcherOptions,这时,Watcher中的逻辑就有变化了:服务器

//这段代码在Watcher类中,文件路径为vue/src/core/observer/watcher.js
if (options) {
    this.deep = !!options.deep
    this.user = !!options.user
    this.lazy = !!options.lazy
    this.sync = !!options.sync
 } else {
    this.deep = this.user = this.lazy = this.sync = false
 }
复制代码

这里的options指的就是computedWatcherOptions,当咱们走initData的逻辑的时候,options并不存在,因此this.lazy = false,但当咱们有了computedWatcherOptions后,this.lazy = true。同时,后面还有这样一段代码:this.dirty = this.lazydirty的值也为true了。ide

this.value = this.lazy
      ? undefined
      : this.get()
复制代码

这段代码咱们能够知道,当lazyfalse时,返回的是undefined而不是this.get()方法。也就是说,并不会执行computed中的两个方法:(请看我开头写的computed示例)函数

function(){
  return this.data_one + 1;
}
function(){
  return this.data_two + this.data_one;
}
复制代码

这也就意味着,computed的值还并无更新。而这个逻辑也就暂时先告一段落。oop

二. defineProperty

让咱们再回到initComputed函数中来:

if (!(key in vm)) {
   //若是computed中的key没有在vm中,经过defineComputed挂载上去
   defineComputed(vm, key, userDef)
} 复制代码

能够看到,当key值没有挂载到vm上时,执行defineComputed函数:

//一个用来组装defineProperty的对象
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function defineComputed ( target: any, key: string, userDef: Object | Function ) {
  //是不是服务端渲染,注意这个变量名 => shouldCache
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    //若是userDef是function,给sharedPropertyDefinition.get也就是当前key的getter
    //赋上createComputedGetter(key)
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    //不然就使用userDef.get和userDef.set赋值
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  //最后,咱们把这个key挂载到vm上
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
复制代码

defineComputed中,先判断是不是服务端渲染,若是不是,说明计算属性是须要缓存的,即shouldCache是为true 。接下来,判断userDef是不是函数,若是是就说明是咱们常规computed的用法,将getter设为createComputedGetter(key)的返回值。若是不是函数,说明这个计算属性是咱们自定义的,须要使用userDef.getuserDef.set来为gettersetter赋值了,这个else部分我就不详细说了,不会到自定义computed的朋友能够看文档计算属性的setter。最后,将computed的这个key挂载到vm上,当你访问这个计算属性时就会调用getter。

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
    }
  }
}复制代码

最后咱们来看createComputedGetter这个函数,他返回了一个函数computedGetter,此时若是watcher存在的状况下,判断watcher.dirty是否存在,根据前面的分析,第一次新建Watcher实例的时候this.dirty是为true的,此时调用watcher.evaluate()

function evaluate () {
    this.value = this.get()
    this.dirty = false
}复制代码

this.get()实际上就是执行计算属性的方法。以后将this.dirty设为false。另外,当咱们执行this.get()时是会为Dep.target赋值的,因此还会执行watcher.depend(),将计算属性的watcher添加到依赖中去。最后返回watcher.value,终于,咱们获取到了计算属性的值,完成了computed的初始化。

三. 计算属性的缓存——lazy Watcher

不过,此时咱们还并无解决本文的重点,也就是"lazy watcher"。还记得Vue官方文档是这样形容computed的:

咱们能够将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是彻底相同的。然而,不一样的是 计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会从新求值。这就意味着只要 message 尚未发生改变,屡次访问 reversedMessage 计算属性会当即返回以前的计算结果,而没必要再次执行函数。

回顾以前的代码,咱们发现只要不更新计算属性的中data属性的值,在第一次获取值后,watch.lazy始终为false,也就永远不会执行watcher.evaluate(),因此这个计算属性永远不会从新求值,一直使用上一次得到(也就是所谓的缓存)的值。

一旦data属性的值发生变化,根据咱们知道会触发update()致使页面从新渲染(这部份内容有点跳,不清楚的朋友必定先弄懂data数据绑定的原理),从新initComputed,那么this.dirty = this.lazy = true,计算属性就会从新取值。

OK,关于computed的原理部分我就说完了,不过这篇文章仍是留了个坑,在createComputedGetter函数中有这样一行代码:

const watcher = this._computedWatchers && this._computedWatchers[key]复制代码

根据上下文咱们能够推测出this._computedWatchers中确定保存着initComputed时建立的watcher实例,但何时把这个实例放到this._computedWatchers中的呢?我尚未找到,若是有知道的朋友请留言分享,你们一块儿讨论,很是感谢!

相关文章
相关标签/搜索