Skip to content

Commit

Permalink
fix(reactivity): avoid exponential perf cost and reduce call stack de…
Browse files Browse the repository at this point in the history
…pth for deeply chained computeds (#11944)

close #11928
  • Loading branch information
yyx990803 authored Sep 16, 2024
1 parent cbc39d5 commit c74bb8c
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 22 deletions.
13 changes: 9 additions & 4 deletions packages/reactivity/src/computed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EffectFlags,
type Subscriber,
activeSub,
batch,
refreshComputed,
} from './effect'
import type { Ref } from './ref'
Expand Down Expand Up @@ -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
}
Expand Down
13 changes: 7 additions & 6 deletions packages/reactivity/src/dep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand All @@ -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()
Expand Down
38 changes: 26 additions & 12 deletions packages/reactivity/src/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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>()
Expand All @@ -92,7 +101,7 @@ export class ReactiveEffect<T = any>
/**
* @internal
*/
nextEffect?: ReactiveEffect = undefined
next?: Subscriber = undefined
/**
* @internal
*/
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down

0 comments on commit c74bb8c

Please sign in to comment.