源码分析:Vue computed 一文读懂计算属性原理

1,256 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

关于Vue侦听属性的原理我们在另一篇文章中已经进行了解析,想了解的可以点击这里。下面我们从源码角度来分析Vue computed的原理。

Computed

computed属性是在initState阶段初始化的,我们直接看源码:

计算属性初始化

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 */)
  }
  // 定义了computed,执行initComputed
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initComputed

const computedWatcherOptions = { computed: true }

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  // 新增_computedWatchers属性为空对象
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
  
  // 遍历computed
  for (const key in computed) {
    // 取值
    const userDef = computed[key]
    // computed可以设置为函数类型,也可以设置为对象设置get属性(如果是对象必须定义get属性)
    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.
      // 非SSR环境,给每个计算属性实例化一个computed Watcher
      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)) {
      // 在vm上未定义,执行defineComputed函数,否则在开发环境报出警告
      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)
      }
    }
  }
}

我们继续看difineComputed函数:

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

这段逻辑是给计算属性添加存储描述符,set属性在设置了时候才会添加,否则就是个空函数,get属性定义的函数是createComputedGetter:

function createComputedGetter (key) {
  return function computedGetter () {
    // 拿到属性对应的watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]
    // 如果watcher存在
    if (watcher) {
      // 收集依赖
      watcher.depend()
      return watcher.evaluate()
    }
  }
}

到这里计算属性的初始化过程就完成了,另外我们再看下计算属性初始化过程中实例化的Watcher是怎么样的:

export default class Watcher {
  ......

  constructor (
    vm: Component,
    expOrFn: string | Function, // computed中定义的函数或是对象中的get函数
    cb: Function, // noop空函数
    options?: ?Object, // { computed: true }
    isRenderWatcher?: boolean // false
  ) {
    this.vm = vm
    ......
    vm._watchers.push(this)
    // options
    if (options) {
      ......
      this.computed = !!options.computed // true
      ......
    }
    ......
    this.cb = cb // noop
    ......
    this.dirty = this.computed // for computed 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
    // 传入的函数赋值给getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } 
    // 实例化Dep,value定义为undefined
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } 
    ......
  }
}

可以看到,computed Watcher实例化的时候,并没有求值,而且实例化了一个Dep。

小结

计算属性初始化的过程中,实例化了计算属性Watcher, 并给计算属性定义了一个getter;实例化计算属性Watcher的时候,没有求值,同时实例化了一个Dep,用来收集订阅者。

计算属性被访问的时候发生了什么:

下面我们列举一个场景,看看computed是如何运行的:

<template>
  <div>{{ message }}</div>
</template>
export default {
  data() {
    return {
      firstName: 'jue'
    }
  },
  computed: {
    message() {
      return this.firstName + 'jin'
    }
  }
}

上面这个组件渲染的时候,会触发render函数,从而访问到计算属性message,这样就触发了计算属性的getter,

// 拿到属性对应的watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
// 如果watcher存在
if (watcher) {
  // 收集依赖
  watcher.depend()
  return watcher.evaluate()
}

计算属性的依赖收集

执行watcher.depend():

/**
* Depend on this watcher. Only for computed property watchers.
* 为计算属性watcher而生
*/
depend () {
  // this指向当前的计算属性Watcher,Dep.target是当前的render Watcher
  if (this.dep && Dep.target) {
    // 计算属性Watcher收集依赖
    this.dep.depend()
  }
}

收集订阅者:

depend () {
  // Dep.target是当前的render Watcher
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}
addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      // 当前的计算属性Watcher收集了订阅者render Watcher,计算属性有变化会通知render Watcher做更新
      dep.addSub(this)
    }
  }
}

在当前的情景下计算属性Watcher收集了订阅者render Watcher,当计算属性有变化的时候会通知render Watcher做更新。

计算属性的计算

watcher.evaluate()
/**
* Evaluate and return the value of the watcher.
* This only gets called for computed property watchers.
* 为计算属性Watcher准备
*/
evaluate () {
  // this.dirty = this.computed = true
  // 第一次触发计算属性getter的时候,evaluate进入该逻辑,作为订阅者订阅通知
  // 当页面渲染中第二次取到计算属性的值,不会重新进行计算,而是直接返回之前计算的结果
  if (this.dirty) {
    // 执行get方法 存放在this.value上
    this.value = this.get()
    this.dirty = false
  }
  // 返回计算结果
  return this.value
}
  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    // this指向计算属性Watcher,将当前的Dep.target赋值为computed Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 执行计算属性中定义的函数,当前情景下是function() { return this.firstName + 'jin' },
      // 这时候会触发响应式数据firstName的getter
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    // 返回计算属性的值
    return value
  }

上面的代码可以看出,这些逻辑会计算出当前计算属性的值,除此之外,当前的Dep.target的值会赋值为计算属性watcher,当计算属性的函数逻辑中涉及到其他的响应式数据的时候(例如本场景中,涉及到了响应式数据firstName),就需要获取该响应式数据(firstName)的值,也就会触发该响应式数据(firstName)的getter方法,从而该响应式数据(firstName)会收集计算属性Watcher。

所以后续当响应式数据(firstName)发生变化的时候,会通知computed Watcher做更新,computed Watcher更新的时候,会通知render Watcher做更新,从而让页面重新渲染。

计算属性缓存,计算结果不变,不会刷新页面

在这里计算属性还有一个优化点,我们继续以上述场景为例子,当我们改变firstName的值的时候,我们继续去看派发更新的源码:

 // dep.notify
 notify () {
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
    // 通知Computed watcher做更新
    subs[i].update()
  }
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
  /* istanbul ignore else */
  // 计算属性watcher 进入该逻辑
  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.
      // 在惰性模式下,除非必要,否则我们不想计算,所以我们简单地把观察者标记为dirty。
      // 实际的计算是在访问computed属性时在this.evaluate()中实时执行的。
      // dirty在初始化computed Watcher的时候会赋值为true,另一个赋值为true的地方就是这儿
      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()
      })
    }
  }
  ......
}

继续看getAndInvoke:

{
  getAndInvoke (cb: Function) {
    // 执行get方法求值
    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 {
        // 值发生变化了,则执行dep.notify,通知render Watcher更新
        cb.call(this.vm, value, oldValue)
      }
    }
  }
}

总结

  1. 计算属性初始化
  • 计算属性初始化的时候实例化了一个Dep,用来收集依赖;
  • 计算属性初始化的时候没有求值;
  1. 计算属性被访问
  • 计算属性被访问的时候会收集订阅者(依赖);
  • 计算属性被访问的时候会作为订阅者订阅通知;
  • 计算属性初次被访问的时候会求值,求值后进入惰性模式;
  1. 计算属性有缓存
  • 当计算属性依赖的响应式数据变化,而计算出的结果不变的情况下,不会通知订阅者更新,也就不会刷新页面