七、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 的实现:

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。

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

例子

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

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

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

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

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

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 方法。

我们首先找到 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 重新渲染,本质上是一种优化。

posted @ 2018-11-29 11:49  快乐~  阅读(344)  评论(0编辑  收藏  举报