vue 中 computed 的原理是什么
Issue 欢迎在 Gtihub Issue 中回答此问题: Issue 91
Author 回答者: wython
要讲清楚,computed原理,首先得讲vue响应式原理,因为computed的实现是基于Watcher对象的。 那么vue的响应式原理是什么呢,众所周知,vue是基于Object.defineProperty实现监听的。在vue初始化数据data和computed数据过程中。会涉及到以下几个对象:
- Observe对象
- Dep对象
- 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
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
}