高级前端
vue
【Q090】vue 中 computed 的原理是什么

vue 中 computed 的原理是什么

Issue 欢迎在 Gtihub Issue 中回答此问题: Issue 91 (opens in a new tab)

Author 回答者: wython (opens in a new tab)

要讲清楚,computed原理,首先得讲vue响应式原理,因为computed的实现是基于Watcher对象的。 那么vue的响应式原理是什么呢,众所周知,vue是基于Object.defineProperty实现监听的。在vue初始化数据data和computed数据过程中。会涉及到以下几个对象:

  1. Observe对象
  2. Dep对象
  3. Watch对象 Observe对象是在data执行响应式时候调用,因为computed属性基于响应式属性,所以其不需要创建Observe对象。 Dep对象主要功能是做依赖收集,有个属性维护多个Watch对象,当更新时候循环调用每个Watch执行更新。 Watch对象主要是用于更新,而且是收集的重点对象。

这里谈到computed计算属性,首先要知道,其有两种定义方式,一种是方法,另一种是get,set属性。而且,其内部监听的对象必须是已经定义响应式的属性,比如data的属性vuex的属性。

vue在创建computed属性时候,会循环所有计算属性,每一个计算属性会创建一个watch,并且在通过defineProperty定义监听,在get中,计算属性工作是做依赖收集,在set中,计算属性重要工作是重新执行计算方法,这里需要多补充一句,因为computed是懒执行,也就是说第一次初始化之后,变不会执行计算,下一次变更执行重新计算是在set中。

另一个补充点是依赖收集的时机,computed收集时机和data一样,是在组件挂载前,但是其收集对象是自己属性对应的watch,而data本身所有数据对应一个watch。

以下附计算属性源码验证说法:

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

可以看到,在执行new Watcher之前,会对计算属性做判断,判断其是否为函数,如果不是则取getter。这是因为计算属性有两种定义方式。之后第二步是执行deineCoumputed。这一步只是简单的调用defineProterty我就不贴代码了。

关于计算属性的getter和setter定义如下: 重点关注get的懒加载部分,和Watcher的定义

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;
    }
  };
}
 
function createGetterInvoker(fn) {
  return function computedGetter() {
    return fn.call(this, this);
  };
}

Author 回答者: MMmaXingXing (opens in a new tab)

Vue3中Computed计算原理和Vue2中使用Watch的搜集方式不太一样了。在Vue3中,如果我们创建了一个computed方法,则会创建一个ComputedRefImpl的对象,在这个对象中进行get和set的处理。如果是读取,就会判断是否是有缓存值以及是否存在不同的数据,如果存在则进行数据的读取(这里对值不变但是频繁读取的情况进行了优化)。判断值已经变化则进行effect的run操作进行取数。设置对应值的话则会进行一个报错提示。

源码如下:

 
import { DebuggerOptions, ReactiveEffect } from './effect'
import { Ref, trackRefValue, triggerRefValue } from './ref'
import { isFunction, NOOP } from '@vue/shared'
import { ReactiveFlags, toRaw } from './reactive'
import { Dep } from './dep'
 
declare const ComputedRefSymbol: unique symbol
 
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
  readonly value: T
  [ComputedRefSymbol]: true
}
 
export interface WritableComputedRef<T> extends Ref<T> {
  readonly effect: ReactiveEffect<T>
}
 
export type ComputedGetter<T> = (...args: any[]) => T
export type ComputedSetter<T> = (v: T) => void
 
export interface WritableComputedOptions<T> {
  get: ComputedGetter<T>
  set: ComputedSetter<T>
}
 
export class ComputedRefImpl<T> {
  public dep?: Dep = undefined
 
  private _value!: T
  public readonly effect: ReactiveEffect<T>
 
  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean
 
  public _dirty = true
  public _cacheable: boolean
 
  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }
 
  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    trackRefValue(self)
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }
 
  set value(newValue: T) {
    this._setter(newValue)
  }
}
 
export function computed<T>(
  getter: ComputedGetter<T>,
  debugOptions?: DebuggerOptions
): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
 
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
 
  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
 
  if (__DEV__ && debugOptions && !isSSR) {
    cRef.effect.onTrack = debugOptions.onTrack
    cRef.effect.onTrigger = debugOptions.onTrigger
  }
 
  return cRef as any
}