美文网首页
vue2.0源码解读 - 计算属性computed

vue2.0源码解读 - 计算属性computed

作者: 小马嗒 | 来源:发表于2019-10-30 22:03 被阅读0次
计算属性 VS 侦听属性

Vue 的组件对象支持了计算属性 computed 和侦听属性 watch 2 个选项,很多同学不了解什么时候该用 computed 什么时候该用 watch。先不回答这个问题,我们接下来从源码实现的角度来分析它们两者有什么区别。

computed

计算属性的初始化是发生在 Vue 实例初始化阶段的 initState 函数中,执行了 if (opts.computed) initComputed(vm, opts.computed),initComputed 的定义在 src/core/instance/state.js 中:

const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  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 // 是否是函数 或者有 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. // internal内部
      watchers[key] = new Watcher(
        vm,
        getter || noop, // noop 空函数
        noop, // noop 空函数
        computedWatcherOptions // lazy: true
      )
    }

    // 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) // userDef = computed[key] 函数
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) { // 计算属性是否在 data 或者 props 中存在
        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 }。computed watcher 和普通 watcher 的差别我稍后会介绍。最后对判断如果 key 不是 vm 的属性,则调用 defineComputed(vm, key, userDef),否则判断计算属性对于的 key 是否已经被 data 或者 prop 所占用,如果是的话则在开发环境报相应的警告。
那么接下来需要重点关注 defineComputed 的实现:

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering() // 服务端渲染
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache // 上面定义 false
      ? 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 值添加 gettersettersetter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下getter 部分,缓存的配置也先忽略,最终 getter 对应的是 createComputedGetter(key) 的返回值,来看一下它的定义:

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,它就是计算属性对应的 getter
整个计算属性的初始化过程到此结束,我们知道计算属性是一个 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` 实例。
然后当我们的 `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() 去求值,来看一下它的定义:

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers. // 只为计算属性量身打造
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false // 视频版本有返回 this.value
  }

evaluate 的逻辑非常简单,通过 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 拿到计算属性对应的值。我们知道了计算属性的求值过程,那么接下来看一下它依赖的数据变化后的逻辑。
一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法:

  /**
   * Subscriber interface.
   * Will be called when a dependency changes. // 
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if ( // 当当前计算的 value 和 上一次的value相同时,则什么都不做,否则当值一样时,仍然执行getter,会重新出发渲染,造成渲染浪费,计算成本是较低的,而重新渲染成本则较高
        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
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else { // 重新出发更新
          this.cb.call(this.vm, value, oldValue) // this.cb = this.deps.notify() // 视频代码中 callback
        }
      }
    }
  }

函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher 重新渲染。
通过以上的分析,我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变花才会触发渲染 watcher 重新渲染,本质上是一种优化。

相关文章

  • vue2.0源码解读 - 计算属性computed

    计算属性 VS 侦听属性Vue 的组件对象支持了计算属性 computed 和侦听属性 watch 2 个选项,很...

  • 计算属性

    1.计算属性get方法: 计算属性(computed)和Methods区别:计算属性(computed)适合:有缓...

  • 重学vue3.0 基础和对比

    setup 和computed 计算属性 ,setup 一般表示值类型的比较多 vue2.0到3.0对应事件变化...

  • Vue复习

    Vue的计算属性 计算属性computed

  • 03.vue3-组合API(下篇)

    组合API-computed函数 定义计算属性: computed函数,是用来定义计算属性的,计算属性不能修改。基...

  • 监听器和计算属性的区别watch,computed

    计算属性computed和监听器watch区别?1.能使用计算属性computed的尽量使用计算属性,但是计算属性...

  • Vue之计算属性computed(一)

    Vue中什么是计算属性computed,计算属性的基础、计算属性computed与方法method实现相同的功能为...

  • 3.vue计算属性和过滤器

    1.计算属性 Vue中的computed属性称为计算属性.它与methods不同,computed是响应式的,调用...

  • Vue

    computed 计算属性 computed的结果会被缓存,除非依赖的响应式属性变化才会重新计算,主要当做属性来使...

  • computed缓存 VS methods方法

    computed 计算属性【选项】 computed 属性会基于它所依赖的数据进行缓存 每个 computed 属...

网友评论

      本文标题:vue2.0源码解读 - 计算属性computed

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