【vue3 源码】四、computed 源码解析
参考代码版本:vue 3.2.37
官网文档:https://vuejs.org/
计算属性。承受一个
getter
函数,并依据getter
函数的返回值返回一个不可变的响应式ref
对象。或者,承受一个具备get
和set
函数的对象,用来创立可写的ref
对象。
文件地位:packages/reactivity/src/computed.ts
应用示例
只读的计算属性:
const count = ref(1)
const doubleCount = computed(() => count.value * 2)
console.log(doubleCount.value) // 2
count.value = 2
console.log(doubleCount.value) // 3
可写的计算属性:
const count = ref(1)
const plusOne = computed({get: () => count.value + 1,
set: (val) => {count.value = val - 1}
})
console.log(plusOne.value) // 2
plusOne.value = 1
console.log(count.value) // 0
进行调试:
const count = ref(1)
const doubleCount = computed(() => count.value * 2, {onTrack(e) {console.log('track')
console.log(e)
},
onTrigger(e) {console.log('trigger')
console.log(e)
}
})
// 触发 track 监听
console.log(doubleCount.value)
// 触发 trigger 监听
count.value++
源码
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
}
computed
接管两个参数,第二参数是依赖收集和触发依赖的钩子函数,只在开发环境中起作用,这里就不做解释了。次要看第一个参数,察看其类型,发现能够传两种参数:一种是一个 getter
函数,一种是个蕴含 get
、set
的对象。
首先从 getterOrOptions
中确定 getter
、setter
(如果getterOrOptions
是个 function
,阐明computed
是不可写的,所以会将 setter
设置为一个空函数),确定好之后,创立一个 ComputedRefImpl
实例,并将其返回。
ComputedRefImpl
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
// 缓存的值
private _value!: T
// 在结构器中创立的 ReactiveEffect 实例
public readonly effect: ReactiveEffect<T>
// 标记为一个 ref 类型
public readonly __v_isRef = true
// 只读标识
public readonly [ReactiveFlags.IS_READONLY]: boolean
// 是否为脏数据,如果是脏数据须要从新计算
public _dirty = true
// 是否可缓存,取决于 SSR
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() {// computed 可能被其余 proxy 包裹,如 readonly(computed(() => foo.bar)),所以要获取 this 的原始对象
const self = toRaw(this)
// 收集依赖
trackRefValue(self)
// 如果是脏数据或者是 SSR,须要从新计算
if (self._dirty || !self._cacheable) {
// _dirty 取 false,避免依赖不变反复计算
self._dirty = false
// 计算
self._value = self.effect.run()!}
return self._value
}
set value(newValue: T) {this._setter(newValue)
}
}
结构器
ComputedRefImpl
结构器接管四个参数:getter
、setter
、isReadonly
(是否只读)、isSSR
(是否为SSR
)。
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.computed = this
// this.effect.active 与 this._cacheable 在 SSR 中为 false
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
在结构器中申明了一个 ReactiveEffect
,并将getter
和一个调度函数作为参数传入,在调度器中如果 _dirty
为false
,会将 _dirty
设置为 true
,并执行triggerRefValue
函数。
triggerRefValue
能够承受两个值:ref
、newVal
。
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {ref = toRaw(ref)
if (ref.dep) {if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {triggerEffects(ref.dep)
}
}
}
triggerRefValue
中首先获取 ref
的原始对象,如果 ref
的原始对象中有 dep
属性,则触发 dep
中的依赖。
在初始化 effect
之后,会将这个 effect
赋给 ComputedRefImpl
实例的 effect
属性,并将 effect.computed
指向 ComputedRefImpl
实例
value 取值函数
get value() {// computed 可能被其余 proxy 包裹,如 readonly(computed(() => foo.bar)),所以要获取 this 的原始对象
const self = toRaw(this)
// 收集依赖
trackRefValue(self)
// 如果是脏数据,须要从新计算
if (self._dirty || !self._cacheable) {
// _dirty 取 false,避免依赖不变反复计算
self._dirty = false
// 计算
self._value = self.effect.run()!}
return self._value
}
当读取 ComputedRefImpl
实例的 value
属性时,因为计算属性可能被其余 proxy 包裹,所以须要应用 toRaw
获取其原始对象。
const self = toRaw(this)
而后调用 trackRefValue
进行依赖的收集。
export function trackRefValue(ref: RefBase<any>) {
// 如果容许收集并且存在 activeEffect 进行依赖收集
if (shouldTrack && activeEffect) {ref = toRaw(ref)
if (__DEV__) {trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
} else {trackEffects(ref.dep || (ref.dep = createDep()))
}
}
}
接着依据 _dirty
与_cacheable
属性来决定是否须要批改 self._value
,其中_dirty
示意是否为脏数据,_cacheable
示意是否能够缓存(取决于是否为服务端渲染,如果为服务端渲染则不能够缓存)。如果是脏数据或不能够被缓存,那么会将 _dirty
设置为false
,并调用self.effect.run()
,批改self._value
。
if (self._dirty || !self._cacheable) {
self._dirty = false
self._value = self.effect.run()!}
最初返回self._value
return self._value
value 存值函数
set value(newValue: T) {this._setter(newValue)
}
当批改 ComputedRefImpl
实例的 value
属性时,会调用实例的 _setter
函数。
到此,你会发现 computed
是懈怠的,只有应用到 computed
的返回后果,能力触发相干计算。
为了加深对 computed
的了解,接下来以一个例子剖析 computed
的缓存及计算过程:
const value = reactive({foo: 1})
const cValue = computed(() => value.foo)
console.log(cValue.value) // 1
value.foo = 2
console.log(cValue.value) // 2
当打印 cValue.value
时,会命中 ComputedRefImpl
对应的 get
办法,在 get
中,执行 trackRefValue
收集对应依赖(因为此时没有处于沉闷状态的 effect
,即activeEffect
,所以并不会进行依赖的收集),默认_dirty
为true
,将 _dirty
设置为 false
,并执行effect.run
,计算数据,计算实现后将数据缓存至selft._vlaue
中,不便下次的利用。在调用 effect.run
过程中,会将在 ComputedRefImpl
结构器中创立的 ReactiveEffect
实例收集到 targetMap[toRaw(value)].foo
中。
当批改 value.foo = 2
,触发targetMap[toRaw(value)].foo
中的依赖,因为在初始化 ReactiveEffect
时,设置了一个调度器,所以在触发依赖过程中会执行这个调度器。这个调度器中会判断如果 _dirty===false
,则将_dirty
设置为 true
,并手动调用triggerRefValue
触发依赖,在调用 triggerRefValue
的过程中,因为cValue.dep=undefined
,所以没有依赖要触发。
当第二次打印 cValue.value
时,因为 _dirty
为true
,所以会执行cValue.effect.run
,并将后果赋值给cValue._value
,最初返回cValue._value
,打印后果2
总结
computed
实质也是个 ref
(ComputedRefImpl
),它是懈怠的,如果不应用计算属性,那么是不会进行计算的,只有应用它,才会调用计算属性中的effect.run
办法进行计算,同时将后果缓存到 _value
中。
computed
如何从新计算?
首先在第一次获取计算属性的值的过程中会进行依赖的收集,假如计算属性的计算与响应式对象的 a、b 两个属性无关,那么会将 computed
中生成的 ReactiveEffect
实例收集到 targetMap[obj].a
、targetMap[obj].b
中,一旦 a
或b
属性变动了,会触发依赖,而在依赖的触发过程中会执行调度函数,在调度函数中会将脏数据的标识 _dirty
设置为 true
,并触发计算属性的依赖。那么在下一次应用到计算属性的话,因为_dirty
为true
,便会调用计算属性中的 effect.run
办法从新计算值。