computed
源码位于 packages/reactivity/src
计算属性是如何实现的呢?
ts
// in computed.ts
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false,
) {
// computed这个API有两种传入参数的方法:
// 1. 只传getter
// 2. 把getter和setter塞进options对象中
// 所以下面的处理就是把对应的getter和setter取出来
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
setter = __DEV__
? () => {
warn('Write operation failed: computed value is readonly')
}
: NOOP // 如果没有传setter则不允许修改
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 构造一个ComputedRefImpl实例
// 参数分别对应 getter, setter, isReadonly, isSSR
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
}
ComputedRefImpl
类 ComputedRefImpl 是如何实现的呢?
注意:在早期的版本中,并没有区分 trigger 和 scheduler。(比如霍春阳书中就是说用 scheduler 来改变 computed 为 dirty、并触发一次 trigger;而后面对 dirty 的判断移动进 computed 的 effect 中,也就是 effect.dirty,并且 triggerEffects 中内置了对 effect 的脏等级设置)
ts
export class ComputedRefImpl<T> {
public dep?: Dep = undefined // 收集的effect
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
public _cacheable: boolean
/**
* Dev only
*/
_warnRecursive?: boolean
constructor(
private getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean,
) {
// 创建一个自己的effect实例
// 第一个参数是fn,第二个参数是trigger,第三个参数是scheduler(这里没有传)
this.effect = new ReactiveEffect(
() => getter(this._value), // fn
() => // trigger
triggerRefValue(
this, // ref
this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect // dirtyLevel
? DirtyLevels.MaybeDirty_ComputedSideEffect
: DirtyLevels.MaybeDirty,
),
)
this.effect.computed = this // 给effect绑定computed
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
get value() {
// ... 这里暂时省略
}
// set value 没什么好说的, 就是把新值传给setter执行
set value(newValue: T) {
this._setter(newValue)
}
// #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
get _dirty() {
return this.effect.dirty
}
set _dirty(v) {
this.effect.dirty = v
}
// #endregion
}
对 value 的 get 劫持
早期版本(v3.4.0)的 get 其实很简单:
ts
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self)
if (!self._cacheable || self.effect.dirty) {
if (hasChanged(self._value, (self._value = self.effect.run()!))) {
triggerRefValue(self, DirtyLevels.ComputedValueDirty)
}
}
return self._value
}
现在详细看看 get 的逻辑:
1. 检查当前computed是否改变,如果改变则触发trigger
第一个 if,查看当前computed是否脏且改变,如果是则 trigger 依赖了当前 computed 的所有 deps。
ts
export class ComputedRefImpl<T> {
// ...
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
if (
// 当前的effect实际上会被其他的ref收集(读取某个ref的时候), 如果这个ref更改则会触发triggerEffects, 从而让effect变脏(可能是dirty,也可能是MayBeDirty,因此需要靠effect实例的dirty的getter方法,进行查询,见下一个代码块)
(!self._cacheable || self.effect.dirty) &&
hasChanged(self._value, (self._value = self.effect.run()!))
) {
triggerRefValue(self, DirtyLevels.Dirty) // 由于依赖的值发生了改变,那么computed的值也就跟着发生了改变,因此要trigger一下computed本身的这个ref
}
// ...
}
// ...
}
读取 self.effect.dirty 实际触发了 getter
ts
export class ReactiveEffect<T = any> {
// ...
public get dirty() {
// 如果是可能脏,则需要triggerComputed来最终确认是否为脏。
if (
this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
this._dirtyLevel === DirtyLevels.MaybeDirty
) {
this._dirtyLevel = DirtyLevels.QueryingDirty // 正在查询是否为脏
pauseTracking()
for (let i = 0; i < this._depsLength; i++) {
// 遍历当前effect被收集进的dep,对这些dep的computed进行trigger
const dep = this.deps[i]
if (dep.computed) {
triggerComputed(dep.computed) // triggerComputed
if (this._dirtyLevel >= DirtyLevels.Dirty) {
// 只要trigger了某个computed后当前的effect._dirtyLevel变"脏"了,说明结果就是脏了,不需要再去trigger其他的effect
break
}
}
}
// 如果结果没变,还是Querying,说明不脏
if (this._dirtyLevel === DirtyLevels.QueryingDirty) {
this._dirtyLevel = DirtyLevels.NotDirty
}
resetTracking()
}
// 返回_dirtyLevel是否为脏
return this._dirtyLevel >= DirtyLevels.Dirty
}
// 设置值的时候,只要不是"不脏"(0),就是"脏"
public set dirty(v) {
this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
}
// ...
}
triggerComputed 做了什么?其实很简单,就是单纯读取一个 computed 的 value,从而再次触发这个 computed 的 get value 劫持
ts
function triggerComputed(computed: ComputedRefImpl<any>) {
return computed.value
}
也就是说,如果是 computed 就会一直查依赖的 computed 是否 dirty,直到找到一个 dirty 的 ref。此时整个链路上的 computed 都会变成 dirty。
复习一下 triggerRefValue 到 triggerEffects 的逻辑:
ts
export function triggerRefValue(
ref: RefBase<any>,
dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
newVal?: any,
) {
ref = toRaw(ref)
const dep = ref.dep
if (dep) {
triggerEffects(
dep,
dirtyLevel,
__DEV__
? {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal,
}
: void 0,
)
}
}
export function triggerEffects(
dep: Dep,
dirtyLevel: DirtyLevels,
debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
pauseScheduling()
for (const effect of dep.keys()) {
// dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
let tracking: boolean | undefined // 只有当前effect和dep中的effect同一轮(trackId相同)才进行tracking
if (
effect._dirtyLevel < dirtyLevel &&
(tracking ??= dep.get(effect) === effect._trackId)
) {
effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty // 由不脏变为其他脏状态, 说明需要调度scheduler
effect._dirtyLevel = dirtyLevel // 更新脏等级
}
if (
effect._shouldSchedule &&
(tracking ??= dep.get(effect) === effect._trackId)
) {
if (__DEV__) {
// eslint-disable-next-line no-restricted-syntax
effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
}
// 需要track和调度, 触发trigger
// 对于普通的ref, 实际上是NOOP; 对于computedRef, 实际上是
// () =>
// triggerRefValue(
// this,
// this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
// ? DirtyLevels.MaybeDirty_ComputedSideEffect
// : DirtyLevels.MaybeDirty,
// ),
effect.trigger() // 所以这里实际上执行了triggerRefValue(对computedRef),而triggerRefValue会转而再执行triggerEffects(对computedRef的dep), 也就是让依赖了当前computedRef的effect进行trigger
//
if (
// 不在运行或者允许递归
(!effect._runnings || effect.allowRecurse) &&
// 脏值不为DirtyLevels.MaybeDirty_ComputedSideEffect
effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
) {
// 说明需要调度, 则将scheduler加入栈中, 然后把shouldSchedule设置为false
effect._shouldSchedule = false
if (effect.scheduler) { // 对于computed,实际没有scheduler,只有trigger
queueEffectSchedulers.push(effect.scheduler)
}
}
}
}
resetScheduling()
}
为什么要有 effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect 的判断?实际上,只有在 computed.value 的 get 劫持的第3步(见下文),会在 trigger 的时候将 dirtyLevel 设置为 MaybeDirty_ComputedSideEffect。这里是为了阻止 computed 的 trigger 再触发 effect/watch/render。见这个PR
2. 重新 track 当前的 computed
这一步和 RefImpl 的 get 是一致的
ts
export class ComputedRefImpl<T> {
// ...
get value() {
// ...
trackRefValue(self) // 追踪computedRef的effect(也就更新成新一轮的trackId)
// ...
}
// ...
}
3. 如果当前 computed 的 effect 还是较脏,则再触发一次 trigger
ts
export class ComputedRefImpl<T> {
// ...
get value() {
// ...
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
if (__DEV__ && (__TEST__ || this._warnRecursive)) {
warn(COMPUTED_SIDE_EFFECT_WARN, `\n\ngetter: `, this.getter)
}
triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
}
// ...
}
// ...
}
4. 最后返回_.value
这一步和 RefImpl 的 get 是一致的
ts
export class ComputedRefImpl<T> {
// ...
get value() {
// ...
return self._value
}
// ...
}
总结
ComputedImpl.value 的 getter:
-
相比于 RefImpl.value 的 getter,在 track 之前和之后各新增了一个步骤:
- 检查当前computed是否改变,如果改变则触发trigger(dirtyLevel为 Dirty)
- 如果当前 computed 的 effect 还是较脏,则再触发一次 trigger(dirtyLevel为MaybeDirty_ComputedSideEffect,这样不会因为这个computed 触发其他的effect.run,防止递归触发)
ComputedImpl.value 的 setter:
- 如果构造函数传入了 setter,则直接执行 setter
- 如果没有传入,则不允许修改(只读)
简单实现一下
具体实现的源码见我的demo仓库:Github - vue-learning
ts
// computed.ts
import { ReactiveEffect } from "./effect.js";
import { trackRefValue, triggerRefValue, type Dep } from "./ref.js";
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
public effect: ReactiveEffect<T>
private _value!: T;
public readonly __v_isRef = true
constructor(public getter: Function, public setter?: Function) {
this.effect = new ReactiveEffect(() => {
console.log("computed.effect 的 fn")
return getter(this._value)
}, () => triggerRefValue(this));
this.effect.computed = this;
this.effect.dirty = true;
}
get value() {
trackRefValue(this);
if (this.effect.dirty) {
console.log(this, "computed dirty, run");
this._value = this.effect.run()
triggerRefValue(this);
}
console.log("获取computed.value");
return this._value;
}
}
export function computed(getter: Function) {
const cRef = new ComputedRefImpl(getter);
return cRef as any;
}
引入使用
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<div>
<span id="ref-value">
</span>
<span id="ref-value-2"></span>
<button id="ref1">点击增加myRef1数值</button>
</div>
<div>
<span id="ref-value-3"></span>
<button id="ref2">点击增加myRef2数值</button>
</div>
<div>
<span id="computed-value">
</span>
<button id="computed">alert myComputed1.value</button>
</div>
<div>
<span id="computed-value-2"></span>
</div>
</div>
<script type="module">
import { ref } from "./ref.js";
import { effect } from "./effect.js";
import { computed } from "./computed.js"
const myRef = ref(666);
effect(() => {
document.querySelector("#ref-value").innerText = "myRef.value = " + myRef.value;
document.querySelector("#ref-value-2").innerText = "after some op:" + (myRef.value % 100 + 10000);
})
const myRef2 = ref(111);
effect(() => {
document.querySelector("#ref-value-3").innerText = "myRef2.value = " + myRef2.value;
})
const myComputed = computed(() => {
console.log("传入computed中的getter");
return myRef.value + myRef2.value
});
effect(() => {
document.querySelector("#computed-value").innerText = "(myRef1.value + myRef2.vale) myComputed.value = " + myComputed.value;
})
const myComputed2 = computed(() => myComputed.value - myRef2.value);
effect(() => {
document.querySelector("#computed-value-2").innerText = "(myComputed.value - myRef2.value) myComputed2.value = " + myComputed2.value;
})
const ref1Btn = document.getElementById("ref1");
ref1Btn.addEventListener("click", () => {
myRef.value++;
console.log("myComputed", myComputed);
})
const ref2Btn = document.getElementById("ref2");
ref2Btn.addEventListener("click", () => {
myRef2.value++;
})
const computed1Btn = document.getElementById("computed");
computed1Btn.addEventListener("click", () => {
alert(myComputed.value);
})
</script>
</body>
</html>
结果