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

View File

@ -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(
{
@ -180,7 +176,12 @@ export class Dep {
}
}
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 {
endBatch()

View File

@ -39,6 +39,9 @@ export interface ReactiveEffectRunner<T = any> {
export let activeSub: Subscriber | undefined
export enum EffectFlags {
/**
* ReactiveEffect only
*/
ACTIVE = 1 << 0,
RUNNING = 1 << 1,
TRACKING = 1 << 2,
@ -69,7 +72,13 @@ export interface Subscriber extends DebuggerOptions {
/**
* @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>()
@ -92,7 +101,7 @@ export class ReactiveEffect<T = any>
/**
* @internal
*/
nextEffect?: ReactiveEffect = undefined
next?: Subscriber = undefined
/**
* @internal
*/
@ -134,9 +143,7 @@ export class ReactiveEffect<T = any>
return
}
if (!(this.flags & EffectFlags.NOTIFIED)) {
this.flags |= EffectFlags.NOTIFIED
this.nextEffect = batchedEffect
batchedEffect = this
batch(this)
}
}
@ -226,7 +233,13 @@ export class ReactiveEffect<T = any>
// }
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 +258,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
}