From 3bc798ee0a8af4befa89e23429302e8ce7c64270 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 16 Sep 2024 13:53:18 +0800 Subject: [PATCH] fix(reactivity): avoid exponential perf cost with deeply chained computeds close #11928 --- packages/reactivity/src/computed.ts | 9 ++++++-- packages/reactivity/src/dep.ts | 6 +---- packages/reactivity/src/effect.ts | 35 ++++++++++++++++++++--------- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index a5f8e5a3c2b..a07d5d52afe 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -5,6 +5,7 @@ import { EffectFlags, type Subscriber, activeSub, + batch, refreshComputed, } from './effect' import type { Ref } from './ref' @@ -111,8 +112,12 @@ export class ComputedRefImpl implements Subscriber { */ notify(): void { this.flags |= EffectFlags.DIRTY - // avoid infinite self recursion - if (activeSub !== this) { + if ( + !(this.flags & EffectFlags.NOTIFIED) && + // avoid infinite self recursion + activeSub !== this + ) { + batch(this) this.dep.notify() } else if (__DEV__) { // TODO warn diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts index 8e4ad1e649e..d6290d69af2 100644 --- a/packages/reactivity/src/dep.ts +++ b/packages/reactivity/src/dep.ts @@ -163,11 +163,7 @@ export class Dep { // original order at the end of the batch, but onTrigger hooks should // be invoked in original order here. for (let head = this.subsHead; head; head = head.nextSub) { - if ( - __DEV__ && - head.sub.onTrigger && - !(head.sub.flags & EffectFlags.NOTIFIED) - ) { + if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) { head.sub.onTrigger( extend( { diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 2dae285d166..7978c3d514b 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -39,6 +39,9 @@ export interface ReactiveEffectRunner { export let activeSub: Subscriber | undefined export enum EffectFlags { + /** + * ReactiveEffect only + */ ACTIVE = 1 << 0, RUNNING = 1 << 1, TRACKING = 1 << 2, @@ -66,6 +69,10 @@ export interface Subscriber extends DebuggerOptions { * @internal */ flags: EffectFlags + /** + * @internal + */ + next?: Subscriber /** * @internal */ @@ -92,7 +99,7 @@ export class ReactiveEffect /** * @internal */ - nextEffect?: ReactiveEffect = undefined + next?: Subscriber = undefined /** * @internal */ @@ -134,9 +141,7 @@ export class ReactiveEffect return } if (!(this.flags & EffectFlags.NOTIFIED)) { - this.flags |= EffectFlags.NOTIFIED - this.nextEffect = batchedEffect - batchedEffect = this + batch(this) } } @@ -226,7 +231,13 @@ export class ReactiveEffect // } let batchDepth = 0 -let batchedEffect: ReactiveEffect | undefined +let batchedSub: Subscriber | undefined + +export function batch(sub: Subscriber): void { + sub.flags |= EffectFlags.NOTIFIED + sub.next = batchedSub + batchedSub = sub +} /** * @internal @@ -245,16 +256,17 @@ export function endBatch(): void { } let error: unknown - while (batchedEffect) { - let e: ReactiveEffect | undefined = batchedEffect - batchedEffect = undefined + while (batchedSub) { + let e: Subscriber | undefined = batchedSub + batchedSub = undefined while (e) { - const next: ReactiveEffect | undefined = e.nextEffect - e.nextEffect = undefined + const next: Subscriber | undefined = e.next + e.next = undefined e.flags &= ~EffectFlags.NOTIFIED if (e.flags & EffectFlags.ACTIVE) { try { - e.trigger() + // ACTIVE flag is effect-only + ;(e as ReactiveEffect).trigger() } catch (err) { if (!error) error = err } @@ -331,6 +343,7 @@ function isDirty(sub: Subscriber): boolean { * @internal */ export function refreshComputed(computed: ComputedRefImpl): undefined { + computed.flags &= ~EffectFlags.NOTIFIED if ( computed.flags & EffectFlags.TRACKING && !(computed.flags & EffectFlags.DIRTY)