vue的源码学习之七——4.计算属性(computed)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qishuixian/article/details/85165177

1 介绍  

         版本:2.5.17 

        我们使用vue-vli创建基于Runtime+Compiler的vue脚手架。

       学习文档:https://ustbhuangyi.github.io/vue-analysis/reactive/computed-watcher.html

2 功能

 我们知道computed实时检测属性有没有发生变化,当发生变化时,相应computed的属性也会随之发生变化。

<script>
export default {
  name: 'App',
    render(createElement) {
      return createElement('div',{
          attrs: {
              id:'app1'
          }
      },this.message)
    },
    data() {
      return {
          message: 'hello vue',
          a: 1,
          b: 2,
      }
    },
    computed: {
      c: function () {
          return this.a + this.c;
      }
    }

}
</script>

3 源码分析

3.1 initState

我们从之前的一章节(vue的源码学习之五——2.数据驱动(new Vue发生了什么)) 知道, Vue 实例初始化阶段,运行initState函数。

其定义在src\core\instance\state.js中:

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

执行了 if (opts.computed) initComputed(vm, opts.computed)。

3.2 initComputed

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in 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
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      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)
      }
    }
  }
}
  • 函数创建 vm._computedWatchers 为一个空对象。
  • 对 computed 对象做遍历,拿到计算属性的每一个 userDef
  • 尝试获取这个 userDef 对应的 getter 函数,拿不到则在开发环境下报警告。
  • 为每一个 getter 创建一个 watcher,这个 watcher 和渲染 watcher 有一点很大的不同,它是一个 computed watcher,因为 const computedWatcherOptions = { computed: true }
  • 对判断如果 key 不是 vm 的属性,则调用 defineComputed(vm, key, userDef),否则判断计算属性对于的 key 是否已经被 data 或者 prop 所占用,如果是的话则在开发环境报相应的警告。

3.3 defineComputed

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    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
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)

这段逻辑很简单,其实就是利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter,setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下 getter 部分,缓存的配置也先忽略,最终 getter 对应的是 createComputedGetter(key) 的返回值。

3.4 createComputedGetter

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

createComputedGetter 返回一个函数 computedGetter,它就是计算属性对应的 getter。

4 computed watcher 与 通的 watcher的区别

4.1 例子分析

我们知道计算属性是一个 computed watcher,它和普通的 watcher 有什么区别呢,为了更加直观,接下来来我们来通过一个例子来分析 computed watcher 的实现。

var vm = new Vue({
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

 当初始化这个 computed watcher 实例的时候,构造函数部分逻辑稍有不同:

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  // ...
  if (this.computed) {
    this.value = undefined
    this.dep = new Dep()
  } else {
    this.value = this.get()
  }
}  

可以发现 computed watcher 会并不会立刻求值,同时持有一个 dep 实例。

4.2 watcher.depend

当我们的 render 函数执行访问到 this.fullName 的时候,就触发了计算属性的 getter,它会拿到计算属性对应的 watcher,然后执行 watcher.depend(),来看一下它的定义:

/**
  * Depend on this watcher. Only for computed property watchers.
  */
depend () {
  if (this.dep && Dep.target) {
    this.dep.depend()
  }
}

注意,这时候的 Dep.target 是渲染 watcher,所以 this.dep.depend() 相当于渲染 watcher 订阅了这个 computed watcher 的变化。然后再执行 watcher.evaluate() 去求值

4.3 evaluate

evaluate()的定义:

/**
  * Evaluate and return the value of the watcher.
  * This only gets called for computed property watchers.
  */
evaluate () {
  if (this.dirty) {
    this.value = this.get()
    this.dirty = false
  }
  return this.value
}

evaluate 的逻辑非常简单:

  • 判断 this.dirty,如果为 true 则通过 this.get() 求值
  • 把 this.dirty 设置为 false。
  • 在求值过程中,会执行 value = this.getter.call(vm, vm),这实际上就是执行了计算属性定义的 getter 函数,
  • 在我们这个例子就是执行了 return this.firstName + ' ' + this.lastName

这里需要特别注意的是,由于 this.firstName 和 this.lastName 都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候 Dep.target 就是这个 computed watcher

最后通过 return this.value 拿到计算属性对应的值。我们知道了计算属性的求值过程,那么接下来看一下它依赖的数据变化后的逻辑。

4.4 update

一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法:

/* istanbul ignore else */
if (this.computed) {
  // A computed property watcher has two modes: lazy and activated.
  // It initializes as lazy by default, and only becomes activated when
  // it is depended on by at least one subscriber, which is typically
  // another computed property or a component's render function.
  if (this.dep.subs.length === 0) {
    // In lazy mode, we don't want to perform computations until necessary,
    // so we simply mark the watcher as dirty. The actual computation is
    // performed just-in-time in this.evaluate() when the computed property
    // is accessed.
    this.dirty = true
  } else {
    // In activated mode, we want to proactively perform the computation
    // but only notify our subscribers when the value has indeed changed.
    this.getAndInvoke(() => {
      this.dep.notify()
    })
  }
} else if (this.sync) {
  this.run()
} else {
  queueWatcher(this)
}

那么对于计算属性这样的 computed watcher,它实际上是有 2 种模式,lazy 和 active。如果 this.dep.subs.length === 0 成立,则说明没有人去订阅这个 computed watcher 的变化,仅仅把 this.dirty = true,只有当下次再访问这个计算属性的时候才会重新求值。在我们的场景下,渲染 watcher 订阅了这个 computed watcher 的变化,那么它会执行:

this.getAndInvoke(() => {
  this.dep.notify()
})

getAndInvoke (cb: Function) {
  const value = this.get()
  if (
    value !== this.value ||
    // Deep watchers and watchers on Object/Arrays should fire even
    // when the value is the same, because the value may
    // have mutated.
    isObject(value) ||
    this.deep
  ) {
    // set new value
    const oldValue = this.value
    this.value = value
    this.dirty = false
    if (this.user) {
      try {
        cb.call(this.vm, value, oldValue)
      } catch (e) {
        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
      }
    } else {
      cb.call(this.vm, value, oldValue)
    }
  }
}

getAndInvoke 函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher 重新渲染。

5 总结

我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher重新渲染,本质上是一种优化。

猜你喜欢

转载自blog.csdn.net/qishuixian/article/details/85165177