From 6b96e090aff8c244cd8f9b86fb8c42c3432bc758 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 6 Feb 2024 18:44:09 +0800 Subject: [PATCH] fix(reactivity): avoid infinite recursion from side effects in computed getter (#10232) close #10214 --- .../reactivity/__tests__/computed.spec.ts | 38 ++++++++++++-- packages/reactivity/src/computed.ts | 12 +++-- packages/reactivity/src/constants.ts | 5 +- packages/reactivity/src/effect.ts | 10 +++- packages/reactivity/src/ref.ts | 51 ++----------------- 5 files changed, 58 insertions(+), 58 deletions(-) diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index 5a0beb973e8..c3d0c7f15ed 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -482,8 +482,12 @@ describe('reactivity/computed', () => { c3.value expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty) - expect(c2.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty) - expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty) + expect(c2.effect._dirtyLevel).toBe( + DirtyLevels.MaybeDirty_ComputedSideEffect, + ) + expect(c3.effect._dirtyLevel).toBe( + DirtyLevels.MaybeDirty_ComputedSideEffect, + ) }) it('should work when chained(ref+computed)', () => { @@ -550,8 +554,12 @@ describe('reactivity/computed', () => { c3.value expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty) - expect(c2.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty) - expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty) + expect(c2.effect._dirtyLevel).toBe( + DirtyLevels.MaybeDirty_ComputedSideEffect, + ) + expect(c3.effect._dirtyLevel).toBe( + DirtyLevels.MaybeDirty_ComputedSideEffect, + ) v1.value.v.value = 999 expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty) @@ -581,4 +589,26 @@ describe('reactivity/computed', () => { await nextTick() expect(serializeInner(root)).toBe(`2`) }) + + it('should not trigger effect scheduler by recurse computed effect', async () => { + const v = ref('Hello') + const c = computed(() => { + v.value += ' World' + return v.value + }) + const Comp = { + setup: () => { + return () => c.value + }, + } + const root = nodeOps.createElement('div') + + render(h(Comp), root) + await nextTick() + expect(serializeInner(root)).toBe('Hello World') + + v.value += ' World' + await nextTick() + expect(serializeInner(root)).toBe('Hello World World World World') + }) }) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 9eed5bc8399..259d4e32c8a 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -43,7 +43,13 @@ export class ComputedRefImpl { ) { this.effect = new ReactiveEffect( () => getter(this._value), - () => triggerRefValue(this, DirtyLevels.MaybeDirty), + () => + triggerRefValue( + this, + this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect + ? DirtyLevels.MaybeDirty_ComputedSideEffect + : DirtyLevels.MaybeDirty, + ), ) this.effect.computed = this this.effect.active = this._cacheable = !isSSR @@ -60,8 +66,8 @@ export class ComputedRefImpl { triggerRefValue(self, DirtyLevels.Dirty) } trackRefValue(self) - if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty) { - triggerRefValue(self, DirtyLevels.MaybeDirty) + if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) { + triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect) } return self._value } diff --git a/packages/reactivity/src/constants.ts b/packages/reactivity/src/constants.ts index d7d745ff7cd..e5350358ae3 100644 --- a/packages/reactivity/src/constants.ts +++ b/packages/reactivity/src/constants.ts @@ -36,6 +36,7 @@ export enum ReactiveFlags { export enum DirtyLevels { NotDirty = 0, QueryingDirty = 1, - MaybeDirty = 2, - Dirty = 3, + MaybeDirty_ComputedSideEffect = 2, + MaybeDirty = 3, + Dirty = 4, } diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index db2dd5e0ca2..3dacce74d50 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -76,7 +76,10 @@ export class ReactiveEffect { } public get dirty() { - if (this._dirtyLevel === DirtyLevels.MaybeDirty) { + if ( + this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect || + this._dirtyLevel === DirtyLevels.MaybeDirty + ) { this._dirtyLevel = DirtyLevels.QueryingDirty pauseTracking() for (let i = 0; i < this._depsLength; i++) { @@ -310,7 +313,10 @@ export function triggerEffects( effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo)) } effect.trigger() - if (!effect._runnings || effect.allowRecurse) { + if ( + (!effect._runnings || effect.allowRecurse) && + effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect + ) { effect._shouldSchedule = false if (effect.scheduler) { queueEffectSchedulers.push(effect.scheduler) diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 2fe11a83f0d..1b9d60ef06b 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -44,27 +44,15 @@ type RefBase = { value: T } -/** - * trackRefValue 用于跟踪依赖 - * @param ref - */ export function trackRefValue(ref: RefBase) { - // 只有在需要跟踪且当前有活跃的effect时,才会进行依赖收集 if (shouldTrack && activeEffect) { - // 获取ref的原始值,去除响应式对象的包装 ref = toRaw(ref) - // trackEffect 用于建立当前活跃的effect与ref之间的依赖关系 trackEffect( - // 当前活跃的effect activeEffect, - // 获取或创建 ref 的依赖管理器 dep - ref.dep || - (ref.dep = createDep( - () => (ref.dep = undefined), - ref instanceof ComputedRefImpl ? ref : undefined, - )), - - // 开发模式,提供额外调试信息 + (ref.dep ??= createDep( + () => (ref.dep = undefined), + ref instanceof ComputedRefImpl ? ref : undefined, + )), __DEV__ ? { target: ref, @@ -76,23 +64,14 @@ export function trackRefValue(ref: RefBase) { } } -/** - * triggerRefValue 用于触发更新 - * @param ref - * @param dirtyLevel - * @param newVal - */ export function triggerRefValue( ref: RefBase, dirtyLevel: DirtyLevels = DirtyLevels.Dirty, newVal?: any, ) { - // 获取ref的原始值,去除响应式对象的包装 ref = toRaw(ref) - // 获取 ref 的依赖管理器 const dep = ref.dep if (dep) { - // triggerEffects 用于出发所有依赖此 ref 的 effect 更新 triggerEffects( dep, dirtyLevel, @@ -169,58 +148,36 @@ function createRef(rawValue: unknown, shallow: boolean) { if (isRef(rawValue)) { return rawValue } - debugger return new RefImpl(rawValue, shallow) } class RefImpl { - // 存储ref的值 private _value: T - // 存储ref的原始值 private _rawValue: T - // 这是一个依赖对象,用于收集依赖,当依赖发生变化时,触发更新 public dep?: Dep = undefined - // 用于标识是否是一个ref对象 public readonly __v_isRef = true constructor( value: T, public readonly __v_isShallow: boolean, ) { - // 使用__v_isShallow标识是否是一个浅层的ref - // 如果是浅层的ref,那么value就是原始值,否则就是一个响应式对象 this._rawValue = __v_isShallow ? value : toRaw(value) this._value = __v_isShallow ? value : toReactive(value) } - // 获取ref的值 - // 如果ref的值是一个响应式对象,那么会对这个响应式对象进行依赖追踪 get value() { - debugger trackRefValue(this) return this._value } - // 设置ref的值 - // 在设置值时,会根据条件决定是否需要转换值 set value(newVal) { - debugger - // 确定是否可以直接使用newVal - // 取决于 __v_isShallow、newVal是否是一个浅层或只读的响应式对象 const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal) - - // 根据 useDirectValue 判断是否需要将 newVal 转换为原始值 newVal = useDirectValue ? newVal : toRaw(newVal) - - // 判断新值是否发生了变化 if (hasChanged(newVal, this._rawValue)) { - // 如果发生了变化,那么就更新值 this._rawValue = newVal this._value = useDirectValue ? newVal : toReactive(newVal) - - // 触发更新 triggerRefValue(this, DirtyLevels.Dirty, newVal) } }