Skip to content

Commit cd12350

Browse files
committed
refactor(reactivity): simplify Effect, EffectScope cleanup behavior
1 parent 335a450 commit cd12350

File tree

5 files changed

+118
-101
lines changed

5 files changed

+118
-101
lines changed

packages/reactivity/__tests__/effectScope.spec.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ describe('reactivity/effect/scope', () => {
3030

3131
it('should work w/ active property', () => {
3232
const scope = effectScope()
33-
scope.run(() => 1)
33+
const src = computed(() => 1)
34+
scope.run(() => src.value)
3435
expect(scope.active).toBe(true)
3536
scope.stop()
3637
expect(scope.active).toBe(false)
@@ -170,17 +171,17 @@ describe('reactivity/effect/scope', () => {
170171

171172
scope.stop()
172173

174+
expect(getEffectsCount(scope)).toBe(0)
175+
173176
scope.run(() => {
174177
effect(() => (doubled = counter.num * 2))
175178
})
176179

177-
expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned()
178-
179-
expect(getEffectsCount(scope)).toBe(0)
180+
expect(getEffectsCount(scope)).toBe(1)
180181

181182
counter.num = 7
182183
expect(dummy).toBe(0)
183-
expect(doubled).toBe(undefined)
184+
expect(doubled).toBe(14)
184185
})
185186

186187
it('should fire onScopeDispose hook', () => {
@@ -359,7 +360,7 @@ describe('reactivity/effect/scope', () => {
359360
expect(cleanupCalls).toBe(1)
360361

361362
expect(getEffectsCount(scope)).toBe(0)
362-
expect(scope.cleanups.length).toBe(0)
363+
expect(scope.cleanups).toBe(0)
363364
})
364365
})
365366

packages/reactivity/src/effect.ts

Lines changed: 68 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { TrackOpTypes, TriggerOpTypes } from './constants'
33
import { setupOnTrigger } from './debug'
44
import { activeEffectScope } from './effectScope'
55
import {
6+
type Dependency,
67
type Link,
78
type Subscriber,
89
SubscriberFlags,
@@ -51,26 +52,24 @@ export enum EffectFlags {
5152
ALLOW_RECURSE = 1 << 7,
5253
PAUSED = 1 << 8,
5354
NOTIFIED = 1 << 9,
54-
STOP = 1 << 10,
5555
}
5656

57-
export class ReactiveEffect<T = any> implements ReactiveEffectOptions {
57+
export class ReactiveEffect<T = any>
58+
implements ReactiveEffectOptions, Dependency, Subscriber
59+
{
5860
// Subscriber
5961
deps: Link | undefined = undefined
6062
depsTail: Link | undefined = undefined
6163
flags: number = 0
64+
cleanups: number = 0
6265

6366
// Dependency
6467
subs: Link | undefined = undefined
6568
subsTail: Link | undefined = undefined
6669

67-
/**
68-
* @internal
69-
*/
70-
cleanup?: () => void = undefined
71-
72-
onStop?: () => void
70+
// dev only
7371
onTrack?: (event: DebuggerEvent) => void
72+
// dev only
7473
onTrigger?: (event: DebuggerEvent) => void
7574

7675
// @ts-expect-error
@@ -80,13 +79,13 @@ export class ReactiveEffect<T = any> implements ReactiveEffectOptions {
8079
if (fn !== undefined) {
8180
this.fn = fn
8281
}
83-
if (activeEffectScope && activeEffectScope.active) {
82+
if (activeEffectScope) {
8483
link(this, activeEffectScope)
8584
}
8685
}
8786

8887
get active(): boolean {
89-
return !(this.flags & EffectFlags.STOP)
88+
return this.deps !== undefined
9089
}
9190

9291
pause(): void {
@@ -122,12 +121,10 @@ export class ReactiveEffect<T = any> implements ReactiveEffectOptions {
122121
}
123122

124123
run(): T {
125-
let flags = this.flags
126-
if (flags & EffectFlags.STOP) {
127-
// stopped during cleanup
128-
return this.fn()
124+
const cleanups = this.cleanups
125+
if (cleanups) {
126+
cleanup(this, cleanups)
129127
}
130-
cleanupEffect(this)
131128
const prevSub = activeSub
132129
setActiveSub(this)
133130
startTracking(this)
@@ -143,7 +140,7 @@ export class ReactiveEffect<T = any> implements ReactiveEffectOptions {
143140
}
144141
setActiveSub(prevSub)
145142
endTracking(this)
146-
flags = this.flags
143+
const flags = this.flags
147144
if (
148145
(flags & (SubscriberFlags.Recursed | EffectFlags.ALLOW_RECURSE)) ===
149146
(SubscriberFlags.Recursed | EffectFlags.ALLOW_RECURSE)
@@ -155,17 +152,15 @@ export class ReactiveEffect<T = any> implements ReactiveEffectOptions {
155152
}
156153

157154
stop(): void {
158-
const flags = this.flags
159-
if (!(flags & EffectFlags.STOP)) {
160-
this.flags = flags | EffectFlags.STOP
161-
startTracking(this)
162-
endTracking(this)
163-
cleanupEffect(this)
164-
this.onStop && this.onStop()
165-
166-
if (this.subs !== undefined) {
167-
unlink(this.subs)
168-
}
155+
const sub = this.subs
156+
const cleanups = this.cleanups
157+
if (sub !== undefined) {
158+
unlink(sub)
159+
}
160+
startTracking(this)
161+
endTracking(this)
162+
if (cleanups) {
163+
cleanup(this, cleanups)
169164
}
170165
}
171166

@@ -205,6 +200,15 @@ export function effect<T = any>(
205200

206201
const e = new ReactiveEffect(fn)
207202
if (options) {
203+
const onStop = options.onStop
204+
if (onStop !== undefined) {
205+
options.onStop = undefined
206+
const stop = e.stop.bind(e)
207+
e.stop = () => {
208+
stop()
209+
onStop()
210+
}
211+
}
208212
extend(e, options)
209213
}
210214
try {
@@ -276,6 +280,32 @@ export function resetTracking(): void {
276280
}
277281
}
278282

283+
const cleanupCbs = new WeakMap()
284+
285+
export function onCleanup(
286+
sub: Subscriber & { cleanups: number },
287+
cb: () => void,
288+
): void {
289+
const cbs = cleanupCbs.get(sub)
290+
if (cbs === undefined) {
291+
cleanupCbs.set(sub, [cb])
292+
sub.cleanups = 1
293+
} else {
294+
cbs[sub.cleanups!++] = cb
295+
}
296+
}
297+
298+
export function cleanup(
299+
sub: Subscriber & { cleanups: number },
300+
length: number,
301+
): void {
302+
const cbs = cleanupCbs.get(sub)!
303+
for (let i = 0; i < length; ++i) {
304+
cbs[i]()
305+
}
306+
sub.cleanups = 0
307+
}
308+
279309
/**
280310
* Registers a cleanup function for the current active effect.
281311
* The cleanup function is called right before the next effect run, or when the
@@ -289,8 +319,9 @@ export function resetTracking(): void {
289319
* an active effect.
290320
*/
291321
export function onEffectCleanup(fn: () => void, failSilently = false): void {
292-
if (activeSub instanceof ReactiveEffect) {
293-
activeSub.cleanup = fn
322+
const e = activeSub
323+
if (e instanceof ReactiveEffect) {
324+
onCleanup(e, () => cleanupEffect(fn))
294325
} else if (__DEV__ && !failSilently) {
295326
warn(
296327
`onEffectCleanup() was called when there was no active effect` +
@@ -299,18 +330,14 @@ export function onEffectCleanup(fn: () => void, failSilently = false): void {
299330
}
300331
}
301332

302-
function cleanupEffect(e: ReactiveEffect) {
303-
const cleanup = e.cleanup
304-
if (cleanup !== undefined) {
305-
e.cleanup = undefined
306-
// run cleanup without active effect
307-
const prevSub = activeSub
308-
activeSub = undefined
309-
try {
310-
cleanup()
311-
} finally {
312-
activeSub = prevSub
313-
}
333+
function cleanupEffect(fn: () => void) {
334+
// run cleanup without active effect
335+
const prevSub = activeSub
336+
activeSub = undefined
337+
try {
338+
fn()
339+
} finally {
340+
activeSub = prevSub
314341
}
315342
}
316343

packages/reactivity/src/effectScope.ts

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { EffectFlags } from './effect'
1+
import { EffectFlags, cleanup, onCleanup } from './effect'
22
import {
33
type Dependency,
44
type Link,
55
type Subscriber,
6+
endTracking,
67
link,
8+
startTracking,
79
unlink,
810
} from './system'
911
import { warn } from './warning'
@@ -15,28 +17,20 @@ export class EffectScope implements Subscriber, Dependency {
1517
deps: Link | undefined = undefined
1618
depsTail: Link | undefined = undefined
1719
flags: number = 0
20+
cleanups: number = 0
1821

1922
// Dependency
2023
subs: Link | undefined = undefined
2124
subsTail: Link | undefined = undefined
2225

23-
/**
24-
* @internal
25-
*/
26-
cleanups: (() => void)[] = []
27-
/**
28-
* @internal
29-
*/
30-
cleanupsLength = 0
31-
3226
constructor(detached = false) {
3327
if (!detached && activeEffectScope) {
3428
link(this, activeEffectScope)
3529
}
3630
}
3731

3832
get active(): boolean {
39-
return !(this.flags & EffectFlags.STOP)
33+
return this.deps !== undefined
4034
}
4135

4236
notify(): void {}
@@ -70,35 +64,25 @@ export class EffectScope implements Subscriber, Dependency {
7064
}
7165

7266
run<T>(fn: () => T): T | undefined {
73-
const flags = this.flags
74-
if (!(flags & EffectFlags.STOP)) {
75-
const prevEffectScope = activeEffectScope
76-
try {
77-
activeEffectScope = this
78-
return fn()
79-
} finally {
80-
activeEffectScope = prevEffectScope
81-
}
82-
} else if (__DEV__) {
83-
warn(`cannot run an inactive effect scope.`)
67+
const prevEffectScope = activeEffectScope
68+
try {
69+
activeEffectScope = this
70+
return fn()
71+
} finally {
72+
activeEffectScope = prevEffectScope
8473
}
8574
}
8675

8776
stop(): void {
88-
const flags = this.flags
89-
if (!(flags & EffectFlags.STOP)) {
90-
this.flags = flags | EffectFlags.STOP
91-
while (this.deps !== undefined) {
92-
unlink(this.deps)
93-
}
94-
const l = this.cleanupsLength
95-
for (let i = 0; i < l; i++) {
96-
this.cleanups[i]()
97-
}
98-
this.cleanupsLength = 0
99-
if (this.subs !== undefined) {
100-
unlink(this.subs)
101-
}
77+
const sub = this.subs
78+
const cleanups = this.cleanups
79+
if (sub !== undefined) {
80+
unlink(sub)
81+
}
82+
startTracking(this)
83+
endTracking(this)
84+
if (cleanups) {
85+
cleanup(this, cleanups)
10286
}
10387
}
10488
}
@@ -141,8 +125,8 @@ export function setCurrentScope(
141125
* @see {@link https://vuejs.org/api/reactivity-advanced.html#onscopedispose}
142126
*/
143127
export function onScopeDispose(fn: () => void, failSilently = false): void {
144-
if (activeEffectScope) {
145-
activeEffectScope.cleanups[activeEffectScope.cleanupsLength++] = fn
128+
if (activeEffectScope !== undefined) {
129+
onCleanup(activeEffectScope, fn)
146130
} else if (__DEV__ && !failSilently) {
147131
warn(
148132
`onScopeDispose() is called when there is no active effect scope` +

0 commit comments

Comments
 (0)