Skip to content

Commit c74bb8c

Browse files
authored
fix(reactivity): avoid exponential perf cost and reduce call stack depth for deeply chained computeds (#11944)
close #11928
1 parent cbc39d5 commit c74bb8c

File tree

3 files changed

+42
-22
lines changed

3 files changed

+42
-22
lines changed

packages/reactivity/src/computed.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
EffectFlags,
66
type Subscriber,
77
activeSub,
8+
batch,
89
refreshComputed,
910
} from './effect'
1011
import type { Ref } from './ref'
@@ -109,11 +110,15 @@ export class ComputedRefImpl<T = any> implements Subscriber {
109110
/**
110111
* @internal
111112
*/
112-
notify(): void {
113+
notify(): true | void {
113114
this.flags |= EffectFlags.DIRTY
114-
// avoid infinite self recursion
115-
if (activeSub !== this) {
116-
this.dep.notify()
115+
if (
116+
!(this.flags & EffectFlags.NOTIFIED) &&
117+
// avoid infinite self recursion
118+
activeSub !== this
119+
) {
120+
batch(this)
121+
return true
117122
} else if (__DEV__) {
118123
// TODO warn
119124
}

packages/reactivity/src/dep.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,7 @@ export class Dep {
163163
// original order at the end of the batch, but onTrigger hooks should
164164
// be invoked in original order here.
165165
for (let head = this.subsHead; head; head = head.nextSub) {
166-
if (
167-
__DEV__ &&
168-
head.sub.onTrigger &&
169-
!(head.sub.flags & EffectFlags.NOTIFIED)
170-
) {
166+
if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
171167
head.sub.onTrigger(
172168
extend(
173169
{
@@ -180,7 +176,12 @@ export class Dep {
180176
}
181177
}
182178
for (let link = this.subs; link; link = link.prevSub) {
183-
link.sub.notify()
179+
if (link.sub.notify()) {
180+
// if notify() returns `true`, this is a computed. Also call notify
181+
// on its dep - it's called here instead of inside computed's notify
182+
// in order to reduce call stack depth.
183+
;(link.sub as ComputedRefImpl).dep.notify()
184+
}
184185
}
185186
} finally {
186187
endBatch()

packages/reactivity/src/effect.ts

+26-12
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export interface ReactiveEffectRunner<T = any> {
3939
export let activeSub: Subscriber | undefined
4040

4141
export enum EffectFlags {
42+
/**
43+
* ReactiveEffect only
44+
*/
4245
ACTIVE = 1 << 0,
4346
RUNNING = 1 << 1,
4447
TRACKING = 1 << 2,
@@ -69,7 +72,13 @@ export interface Subscriber extends DebuggerOptions {
6972
/**
7073
* @internal
7174
*/
72-
notify(): void
75+
next?: Subscriber
76+
/**
77+
* returning `true` indicates it's a computed that needs to call notify
78+
* on its dep too
79+
* @internal
80+
*/
81+
notify(): true | void
7382
}
7483

7584
const pausedQueueEffects = new WeakSet<ReactiveEffect>()
@@ -92,7 +101,7 @@ export class ReactiveEffect<T = any>
92101
/**
93102
* @internal
94103
*/
95-
nextEffect?: ReactiveEffect = undefined
104+
next?: Subscriber = undefined
96105
/**
97106
* @internal
98107
*/
@@ -134,9 +143,7 @@ export class ReactiveEffect<T = any>
134143
return
135144
}
136145
if (!(this.flags & EffectFlags.NOTIFIED)) {
137-
this.flags |= EffectFlags.NOTIFIED
138-
this.nextEffect = batchedEffect
139-
batchedEffect = this
146+
batch(this)
140147
}
141148
}
142149

@@ -226,7 +233,13 @@ export class ReactiveEffect<T = any>
226233
// }
227234

228235
let batchDepth = 0
229-
let batchedEffect: ReactiveEffect | undefined
236+
let batchedSub: Subscriber | undefined
237+
238+
export function batch(sub: Subscriber): void {
239+
sub.flags |= EffectFlags.NOTIFIED
240+
sub.next = batchedSub
241+
batchedSub = sub
242+
}
230243

231244
/**
232245
* @internal
@@ -245,16 +258,17 @@ export function endBatch(): void {
245258
}
246259

247260
let error: unknown
248-
while (batchedEffect) {
249-
let e: ReactiveEffect | undefined = batchedEffect
250-
batchedEffect = undefined
261+
while (batchedSub) {
262+
let e: Subscriber | undefined = batchedSub
263+
batchedSub = undefined
251264
while (e) {
252-
const next: ReactiveEffect | undefined = e.nextEffect
253-
e.nextEffect = undefined
265+
const next: Subscriber | undefined = e.next
266+
e.next = undefined
254267
e.flags &= ~EffectFlags.NOTIFIED
255268
if (e.flags & EffectFlags.ACTIVE) {
256269
try {
257-
e.trigger()
270+
// ACTIVE flag is effect-only
271+
;(e as ReactiveEffect).trigger()
258272
} catch (err) {
259273
if (!error) error = err
260274
}

0 commit comments

Comments
 (0)