vue源码分析(十)

879 阅读3分钟

六、计算属性

initState函数中,会调用initComputed来初始化computed,initComputed函数首先通过Object.create(null)创建一个没有原型对象和构造函数的空对象,然后遍历传入的第二个参数 computed拿到computed对象中的每一个值,computed有两种写法,一种是直接写一个求值函数,另一种是写一个对象,其中有get等于一个求值函数,所以这里通过typeof进行判断,如果是一个函数,那么让getter等于这个函数,否则等于他的get。之后通过new Watcher的方式,定义一个computed watcher。最后调用defineComputed方法,defineComputed函数中shouldCache的值首先定义true(浏览器环境),然后通过typeof来判断传入的userDef的类型,之后定义sharedPropertyDefinition的get为createComputedGetter(key), 执行createComputedGetter(key)createComputedGetter函数返回了computedGetter函数,最终返回computed wathcer的value。如果有set,那么直接让sharedPropertyDefinition 的set等于传入的userDef的set,最终通过Object.defineProperty(target, key, sharedPropertyDefinition)把computed绑定到vm上。所以传入的computed的写法最终也是通过Object.defineProperty进行了数据劫持,在访问的时候,通过get去执行对应的computed watcher的逻辑

// src/core/instance/state.js
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) {
     ...
    }

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

关键的两步,第一步,通过 watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) vm._computedWatchers进行对应的赋值。在new Watcher的过程中,第一个参数传入vm实例,第二个参数传入getter也就是computed的get函数,第三个参数传空也就是说compoted watcher没有回调函数,第四个参数传入computedWatcherOptions也就是 { lazy: true },执行watcher的constructor,把当前的computed watcher push到vm._watcher,之后this.lazythis.dirty为true,this.getter 等于传入的computed函数,最后this.value = this.lazy?undefined:this.get()在首次this.lazy为true,所以首次,this.value为undefined也就是说并不会直接求值

// src/core/observer/watcher.js
/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
     ...
    }
    ...
    // options
    if (options) {
      ...
      this.lazy = !!options.lazy
      ...
    } else {
      ...
    }
    ...
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

那当在渲染的过程中,访问到了coumputed的时候,就会触发computedGetter函数,首次访问watcher.dirty为true,会执行watcher.evaluate(),watcher的evaluate函数,首先会执行this.value = this.get()去调用get函数进行求值,也就是会让watcher的value等于getter函数(之前传入的computed get函数)的返回值,执行get之后,会判断是否有Dep.target,在此处的Dep.target为渲染watcher,他会执行watcher.depend()watcher.depend()会对当前computed的this.deps数组中的值调用depend,也就是调用Depdependthis.deps在执行evaluate的时候,会触发对应响应式的依赖收集,也就是说当前的coumoted watcher中的deps已经存储了依赖的数据的dep。那么在执行computed watcher的depend的时候,会把这些dep执行depend方法,当前的Dep.target是渲染watcher,也就是说渲染wathcer订阅了computed的依赖

// src/core/instance/state.js
 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
    }
  }

当依赖的值发生了改变,也就是触发了dep.notify(),他会先拿到computed wathcer,首次进入watcher的update会先把this.dirty重新设置为true,在之后的notify会触发渲染watcher的queueWatcher进行页面的更新。重新获取渲染watcher后,再次调用之前的逻辑,进行computed watcher的初始化。当前的vue版本是2.6.12。为什么说computed是有缓存的,是因为当调用computedGetter函数,watcher.dirty为true才会重新调用watcher.evaluate()进行求值,当执行过一次watcher.evaluate()后,watcher.dirty为false,则不会再次调用watcher.evaluate()而是直接return watcher.value