fix(reactivity): avoid exponential perf cost and reduce call stack depth for deeply chained computeds (#11944)

close #11928
This commit is contained in:
Evan You 2024-09-16 16:00:31 +08:00 committed by GitHub
parent cbc39d54f0
commit c74bb8c2dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 42 additions and 22 deletions

View File

@ -5,6 +5,7 @@ import {
EffectFlags, EffectFlags,
type Subscriber, type Subscriber,
activeSub, activeSub,
batch,
refreshComputed, refreshComputed,
} from './effect' } from './effect'
import type { Ref } from './ref' import type { Ref } from './ref'
@ -109,11 +110,15 @@ export class ComputedRefImpl<T = any> implements Subscriber {
/** /**
* @internal * @internal
*/ */
notify(): void { notify(): true | void {
this.flags |= EffectFlags.DIRTY this.flags |= EffectFlags.DIRTY
// avoid infinite self recursion if (
if (activeSub !== this) { !(this.flags & EffectFlags.NOTIFIED) &&
this.dep.notify() // avoid infinite self recursion
activeSub !== this
) {
batch(this)
return true
} else if (__DEV__) { } else if (__DEV__) {
// TODO warn // TODO warn
} }

View File

@ -163,11 +163,7 @@ export class Dep {
// original order at the end of the batch, but onTrigger hooks should // original order at the end of the batch, but onTrigger hooks should
// be invoked in original order here. // be invoked in original order here.
for (let head = this.subsHead; head; head = head.nextSub) { for (let head = this.subsHead; head; head = head.nextSub) {
if ( if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
__DEV__ &&
head.sub.onTrigger &&
!(head.sub.flags & EffectFlags.NOTIFIED)
) {
head.sub.onTrigger( head.sub.onTrigger(
extend( extend(
{ {
@ -180,7 +176,12 @@ export class Dep {
} }
} }
for (let link = this.subs; link; link = link.prevSub) { for (let link = this.subs; link; link = link.prevSub) {
link.sub.notify() if (link.sub.notify()) {
// if notify() returns `true`, this is a computed. Also call notify
// on its dep - it's called here instead of inside computed's notify
// in order to reduce call stack depth.
;(link.sub as ComputedRefImpl).dep.notify()
}
} }
} finally { } finally {
endBatch() endBatch()

View File

@ -39,6 +39,9 @@ export interface ReactiveEffectRunner<T = any> {
export let activeSub: Subscriber | undefined export let activeSub: Subscriber | undefined
export enum EffectFlags { export enum EffectFlags {
/**
* ReactiveEffect only
*/
ACTIVE = 1 << 0, ACTIVE = 1 << 0,
RUNNING = 1 << 1, RUNNING = 1 << 1,
TRACKING = 1 << 2, TRACKING = 1 << 2,
@ -69,7 +72,13 @@ export interface Subscriber extends DebuggerOptions {
/** /**
* @internal * @internal
*/ */
notify(): void next?: Subscriber
/**
* returning `true` indicates it's a computed that needs to call notify
* on its dep too
* @internal
*/
notify(): true | void
} }
const pausedQueueEffects = new WeakSet<ReactiveEffect>() const pausedQueueEffects = new WeakSet<ReactiveEffect>()
@ -92,7 +101,7 @@ export class ReactiveEffect<T = any>
/** /**
* @internal * @internal
*/ */
nextEffect?: ReactiveEffect = undefined next?: Subscriber = undefined
/** /**
* @internal * @internal
*/ */
@ -134,9 +143,7 @@ export class ReactiveEffect<T = any>
return return
} }
if (!(this.flags & EffectFlags.NOTIFIED)) { if (!(this.flags & EffectFlags.NOTIFIED)) {
this.flags |= EffectFlags.NOTIFIED batch(this)
this.nextEffect = batchedEffect
batchedEffect = this
} }
} }
@ -226,7 +233,13 @@ export class ReactiveEffect<T = any>
// } // }
let batchDepth = 0 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 * @internal
@ -245,16 +258,17 @@ export function endBatch(): void {
} }
let error: unknown let error: unknown
while (batchedEffect) { while (batchedSub) {
let e: ReactiveEffect | undefined = batchedEffect let e: Subscriber | undefined = batchedSub
batchedEffect = undefined batchedSub = undefined
while (e) { while (e) {
const next: ReactiveEffect | undefined = e.nextEffect const next: Subscriber | undefined = e.next
e.nextEffect = undefined e.next = undefined
e.flags &= ~EffectFlags.NOTIFIED e.flags &= ~EffectFlags.NOTIFIED
if (e.flags & EffectFlags.ACTIVE) { if (e.flags & EffectFlags.ACTIVE) {
try { try {
e.trigger() // ACTIVE flag is effect-only
;(e as ReactiveEffect).trigger()
} catch (err) { } catch (err) {
if (!error) error = err if (!error) error = err
} }