7、vue计算属性

image

细节流程图

image

初始化

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

const computedWatcherOptions = { computed: true }
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 }。computed watcher 和普通 watcher 的差异我稍后会介绍。最后对判断若是 key 不是 vm 的属性,则调用 defineComputed(vm, key, userDef),不然判断计算属性对于的 key 是否已经被 data 或者 prop 所占用,若是是的话则在开发环境报相应的警告。函数

接下来须要重点关注 defineComputed 的实现:oop

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) 的返回值,来看一下它的定义:优化

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

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

整个计算属性的初始化过程到此结束,咱们知道计算属性是一个 computed watcher,它和普通的 watcher 有什么区别呢,为了更加直观,接下来来咱们来经过一个例子来分析 computed watcher 的实现。lua

例子

以上关于计算属性相关初始化工做已经完成了,初始化计算属性的过程当中主要建立了计算属性观察者以及将计算属性定义到组件实例对象上,接下来咱们将经过一些例子来分析计算属性是如何实现的,假设咱们有以下代码:prototype

data () {
  return {
    a: 1
  }
},
computed: {
  compA () {
    return this.a + 1
  }
}

如上代码中,咱们定义了本地数据 data,它拥有一个响应式的属性 a,咱们还定义了计算属性 compA,它的值将依据 a 的值来计算求得。另外咱们假设有以下模板:设计

<div>{{compA}}</div>

模板中咱们使用到了计算属性,咱们知道模板会被编译成渲染函数,渲染函数的执行将触发计算属性 compA 的 get 拦截器函数,那么 compA 的拦截器函数是什么呢?就是咱们前面分析的 sharedPropertyDefinition.get 函数,咱们知道在非服务端渲染的状况下,这个函数为:code

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

也就是说当 compA 属性被读取时,computedGetter 函数将会执行,在 computedGetter 函数内部,首先定义了 watcher 常量,它的值为计算属性 compA 的观察者对象,紧接着若是该观察者对象存在,则会分别执行观察者对象的 depend 方法和 evaluate 方法。component

咱们首先找到 Watcher 类的 depend 方法,以下:

depend () {
  if (this.dep && Dep.target) {
    this.dep.depend()
  }
}

depend 方法的内容很简单,检查 this.dep 和 Dep.target 是否所有有值,若是都有值的状况下便会执行 this.dep.depend 方法。这里咱们首先要知道 this.dep 属性是什么,实际上计算属性的观察者与其余观察者对象不一样,不一样之处首先会体如今建立观察者实例对象的时候,以下是 Watcher 类的 constructor 方法中的一段代码:

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()
  }
}

如上高亮代码所示,当建立计算属性观察者对象时,因为第四个选项参数中 options.computed 为真,因此计算属性观察者对象的 this.computed 属性的值也会为真,因此对于计算属性的观察者来说,在建立时会执行 if 条件分支内的代码,而对于其余观察者对象则会执行 else 分支内的代码。同时咱们可以看到在 else 分支内直接调用 this.get() 方法求值,而 if 分支内并无调用 this.get() 方法求值,而是定义了 this.dep 属性,它的值是一个新建立的 Dep 实例对象。这说明计算属性的观察者是一个惰性求值的观察者。

如今咱们再回到 Watcher 类的 depend 方法中:

depend () {
  if (this.dep && Dep.target) {
    this.dep.depend()
  }
}

此时咱们已经知道了 this.dep 属性是一个 Dep 实例对象,因此 this.dep.depend() 这句代码的做用就是用来收集依赖。那么它收集到的东西是什么呢?这就要看 Dep.target 属性的值是什么了,咱们回想一下整个过程:首先渲染函数的执行会读取计算属性 compA 的值,从而触发计算属性 compA 的 get 拦截器函数,最终调用了 this.dep.depend() 方法收集依赖。这个过程当中的关键一步就是渲染函数的执行,咱们知道在渲染函数执行以前 Dep.target 的值必然是 渲染函数的观察者对象。因此计算属性观察者对象的 this.dep 属性中所收集的就是渲染函数的观察者对象。

记得此时计算属性观察者对象的 this.dep 中所收集的是渲染函数观察者对象,假设咱们把渲染函数观察者对象称为 renderWatcher,那么:

this.dep.subs = [renderWatcher]

这样 computedGetter 函数中的 watcher.depend() 语句咱们就讲解完了,但 computedGetter 函数还没执行完,接下来要执行的是 watcher.evaluate() 语句:

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

咱们找到 Watcher 类的 evaluate 方法看看它作了哪些事情,以下:

evaluate () {
  if (this.dirty) {
    this.value = this.get()
    this.dirty = false
  }
  return this.value
}

咱们知道计算属性的观察者是惰性求值,因此在建立计算属性观察者时除了 watcher.computed 属性为 true 以外,watcher.dirty 属性的值也为 true,表明着当前观察者对象没有被求值,而 evaluate 方法的做用就是用来手动求值的。能够看到在 evaluate 方法内部对 this.dirty 属性作了真假判断,若是为真则调用观察者对象的 this.get 方法求值,同时将this.dirty 属性重置为 false。最后将求得的值返回:return this.value。

这段代码的关键在于求值的这句代码,以下高亮部分所示:

evaluate () {
  if (this.dirty) {
>     this.value = this.get()
    this.dirty = false
  }
  return this.value
}

咱们在计算属性的初始化一节中讲过了,在建立计算属性观察者对象时传递给 Watcher 类的第二个参数为 getter 常量,它的值就是开发者在定义计算属性时的函数(或 userDef.get),以下高亮代码所示:

function initComputed (vm: Component, computed: Object) {
  // 省略...
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // 省略...

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

    // 省略...
  }
}

因此在 evaluate 方法中求值的那句代码最终所执行的求值函数就是用户定义的计算属性的 get 函数。举个例子,假设咱们这样定义计算属性:

computed: {
  compA () {
    return this.a +1
  }
}

那么对于计算属性 compA 来说,执行其计算属性观察者对象的 wather.evaluate 方法求值时,本质上就是执行以下函数进行求值:

compA () {
  return this.a +1
}

你们想想这个函数的执行会发生什么事情?咱们知道数据对象的 a 属性是响应式的,因此如上函数的执行将会触发属性 a 的 get 拦截器函数。因此这会致使属性 a 将会收集到一个依赖,这个依赖实际上就是计算属性的观察者对象。

如今思路大概明朗了,若是计算属性 compA 依赖了数据对象的 a 属性,那么属性 a 将收集计算属性 compA 的 计算属性观察者对象,而 计算属性观察者对象 将收集 渲染函数观察者对象,整个路线是这样的:

假如此时咱们修改响应式属性 a 的值,那么将触发属性 a 所收集的全部依赖,这其中包括计算属性的观察者。咱们知道触发某个响应式属性的依赖实际上就是执行该属性所收集到的全部观察者的 update 方法,如今咱们就找到 Watcher 类的 update 方法,以下:

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)
  }
}

如上高亮代码所示,因为响应式数据收集到了计算属性观察者对象,因此当计算属性观察者对象的 update 方法被执行时,如上 if 语句块的代码将被执行,由于 this.computed 属性为真。接着检查了 this.dep.subs.length === 0 的真假,咱们知道既然是计算属性的观察者,那么 this.dep 中将收集渲染函数做为依赖(或其余观察该计算属性变化的观察者对象做为依赖),因此当依赖的数量不为 0 时,在 else 语句块内会调用 this.dep.notify() 方法继续触发响应,这会致使 this.dep.subs 属性中收集到的全部观察者对象的更新,若是此时 this.dep.subs 中包含渲染函数的观察者,那么这就会致使从新渲染,最终完成视图的更新。

以上就是计算属性的实现思路,本质上计算属性观察者对象就是一个桥梁,它搭建在响应式数据与渲染函数观察者中间,另外你们注意上面的代码中并不是直接调用 this.dep.notify() 方法触发响应,而是将这个方法做为 this.getAndInvoke 方法的回调去执行的,为何这么作呢?那是由于 this.getAndInvoke 方法会从新求值并对比新旧值是否相同,若是知足相同条件则不会触发响应,只有当值确实变化时才会触发响应,这就是文档中的描述,如今你明白了吧:

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

相关文章
相关标签/搜索