Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4ba0d3b

Browse files
committedApr 2, 2025·
refactor(reactivity): associate effects and effect scopes based on doubly linked list (deps, subs)
1 parent 9ab8e4c commit 4ba0d3b

File tree

10 files changed

+255
-270
lines changed

10 files changed

+255
-270
lines changed
 

‎packages/reactivity/__tests__/computed.spec.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -467,12 +467,8 @@ describe('reactivity/computed', () => {
467467
const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
468468

469469
c2.value
470-
expect(
471-
c1.flags & (SubscriberFlags.Dirty | SubscriberFlags.PendingComputed),
472-
).toBe(0)
473-
expect(
474-
c2.flags & (SubscriberFlags.Dirty | SubscriberFlags.PendingComputed),
475-
).toBe(0)
470+
expect(c1.flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)).toBe(0)
471+
expect(c2.flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)).toBe(0)
476472
})
477473

478474
it('should chained computeds dirtyLevel update with first computed effect', () => {

‎packages/reactivity/__tests__/effectScope.spec.ts

+23-14
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('reactivity/effect/scope', () => {
2020

2121
it('should accept zero argument', () => {
2222
const scope = effectScope()
23-
expect(scope.effects.length).toBe(0)
23+
expect(getEffectsCount(scope)).toBe(0)
2424
})
2525

2626
it('should return run value', () => {
@@ -47,7 +47,7 @@ describe('reactivity/effect/scope', () => {
4747
expect(dummy).toBe(7)
4848
})
4949

50-
expect(scope.effects.length).toBe(1)
50+
expect(getEffectsCount(scope)).toBe(1)
5151
})
5252

5353
it('stop', () => {
@@ -60,7 +60,7 @@ describe('reactivity/effect/scope', () => {
6060
effect(() => (doubled = counter.num * 2))
6161
})
6262

63-
expect(scope.effects.length).toBe(2)
63+
expect(getEffectsCount(scope)).toBe(2)
6464

6565
expect(dummy).toBe(0)
6666
counter.num = 7
@@ -87,9 +87,8 @@ describe('reactivity/effect/scope', () => {
8787
})
8888
})
8989

90-
expect(scope.effects.length).toBe(1)
91-
expect(scope.scopes!.length).toBe(1)
92-
expect(scope.scopes![0]).toBeInstanceOf(EffectScope)
90+
expect(getEffectsCount(scope)).toBe(2)
91+
expect(scope.deps?.nextDep?.dep).toBeInstanceOf(EffectScope)
9392

9493
expect(dummy).toBe(0)
9594
counter.num = 7
@@ -117,7 +116,7 @@ describe('reactivity/effect/scope', () => {
117116
})
118117
})
119118

120-
expect(scope.effects.length).toBe(1)
119+
expect(getEffectsCount(scope)).toBe(1)
121120

122121
expect(dummy).toBe(0)
123122
counter.num = 7
@@ -142,13 +141,13 @@ describe('reactivity/effect/scope', () => {
142141
effect(() => (dummy = counter.num))
143142
})
144143

145-
expect(scope.effects.length).toBe(1)
144+
expect(getEffectsCount(scope)).toBe(1)
146145

147146
scope.run(() => {
148147
effect(() => (doubled = counter.num * 2))
149148
})
150149

151-
expect(scope.effects.length).toBe(2)
150+
expect(getEffectsCount(scope)).toBe(2)
152151

153152
counter.num = 7
154153
expect(dummy).toBe(7)
@@ -166,7 +165,7 @@ describe('reactivity/effect/scope', () => {
166165
effect(() => (dummy = counter.num))
167166
})
168167

169-
expect(scope.effects.length).toBe(1)
168+
expect(getEffectsCount(scope)).toBe(1)
170169

171170
scope.stop()
172171

@@ -176,7 +175,7 @@ describe('reactivity/effect/scope', () => {
176175

177176
expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned()
178177

179-
expect(scope.effects.length).toBe(0)
178+
expect(getEffectsCount(scope)).toBe(0)
180179

181180
counter.num = 7
182181
expect(dummy).toBe(0)
@@ -224,9 +223,9 @@ describe('reactivity/effect/scope', () => {
224223
it('should dereference child scope from parent scope after stopping child scope (no memleaks)', () => {
225224
const parent = effectScope()
226225
const child = parent.run(() => effectScope())!
227-
expect(parent.scopes!.includes(child)).toBe(true)
226+
expect(parent.deps?.dep).toBe(child)
228227
child.stop()
229-
expect(parent.scopes!.includes(child)).toBe(false)
228+
expect(parent.deps).toBeUndefined()
230229
})
231230

232231
it('test with higher level APIs', async () => {
@@ -372,7 +371,17 @@ describe('reactivity/effect/scope', () => {
372371
expect(watcherCalls).toBe(3)
373372
expect(cleanupCalls).toBe(1)
374373

375-
expect(scope.effects.length).toBe(0)
374+
expect(getEffectsCount(scope)).toBe(0)
376375
expect(scope.cleanups.length).toBe(0)
377376
})
378377
})
378+
379+
function getEffectsCount(scope: EffectScope): number {
380+
let n = 0
381+
for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
382+
if ('notify' in dep.dep) {
383+
n++
384+
}
385+
}
386+
return n
387+
}

‎packages/reactivity/src/computed.ts

+11-10
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import {
1414
type Link,
1515
type Subscriber,
1616
SubscriberFlags,
17+
checkDirty,
1718
endTracking,
1819
link,
1920
processComputedUpdate,
2021
startTracking,
21-
updateDirtyFlag,
2222
} from './system'
2323
import { warn } from './warning'
2424

@@ -66,7 +66,7 @@ export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
6666
// Subscriber
6767
deps: Link | undefined = undefined
6868
depsTail: Link | undefined = undefined
69-
flags: SubscriberFlags = SubscriberFlags.Computed | SubscriberFlags.Dirty
69+
flags: SubscriberFlags = SubscriberFlags.Dirty
7070

7171
/**
7272
* @internal
@@ -93,12 +93,13 @@ export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
9393
*/
9494
get _dirty(): boolean {
9595
const flags = this.flags
96-
if (
97-
flags & SubscriberFlags.Dirty ||
98-
(flags & SubscriberFlags.PendingComputed &&
99-
updateDirtyFlag(this, this.flags))
100-
) {
101-
return true
96+
if (flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) {
97+
if (flags & SubscriberFlags.Dirty || checkDirty(this.deps!)) {
98+
this.flags = flags | SubscriberFlags.Dirty
99+
return true
100+
} else {
101+
this.flags = flags & ~SubscriberFlags.Pending
102+
}
102103
}
103104
return false
104105
}
@@ -110,7 +111,7 @@ export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
110111
if (v) {
111112
this.flags |= SubscriberFlags.Dirty
112113
} else {
113-
this.flags &= ~(SubscriberFlags.Dirty | SubscriberFlags.PendingComputed)
114+
this.flags &= ~(SubscriberFlags.Dirty | SubscriberFlags.Pending)
114115
}
115116
}
116117

@@ -134,7 +135,7 @@ export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
134135

135136
get value(): T {
136137
const flags = this.flags
137-
if (flags & (SubscriberFlags.Dirty | SubscriberFlags.PendingComputed)) {
138+
if (flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) {
138139
processComputedUpdate(this, flags)
139140
}
140141
if (activeSub !== undefined) {

‎packages/reactivity/src/effect.ts

+25-12
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import {
66
type Link,
77
type Subscriber,
88
SubscriberFlags,
9+
checkDirty,
910
endTracking,
11+
link,
1012
startTracking,
11-
updateDirtyFlag,
13+
unlink,
1214
} from './system'
1315
import { warn } from './warning'
1416

@@ -56,7 +58,11 @@ export class ReactiveEffect<T = any> implements ReactiveEffectOptions {
5658
// Subscriber
5759
deps: Link | undefined = undefined
5860
depsTail: Link | undefined = undefined
59-
flags: number = SubscriberFlags.Effect
61+
flags: number = 0
62+
63+
// Dependency
64+
subs: Link | undefined = undefined
65+
subsTail: Link | undefined = undefined
6066

6167
/**
6268
* @internal
@@ -69,7 +75,7 @@ export class ReactiveEffect<T = any> implements ReactiveEffectOptions {
6975

7076
constructor(public fn: () => T) {
7177
if (activeEffectScope && activeEffectScope.active) {
72-
activeEffectScope.effects.push(this)
78+
link(this, activeEffectScope)
7379
}
7480
}
7581

@@ -99,7 +105,7 @@ export class ReactiveEffect<T = any> implements ReactiveEffectOptions {
99105
if (!(flags & EffectFlags.PAUSED)) {
100106
this.scheduler()
101107
} else {
102-
this.flags |= EffectFlags.NOTIFIED
108+
this.flags = flags | EffectFlags.NOTIFIED
103109
}
104110
}
105111

@@ -132,11 +138,12 @@ export class ReactiveEffect<T = any> implements ReactiveEffectOptions {
132138
}
133139
setActiveSub(prevSub)
134140
endTracking(this)
141+
const flags = this.flags
135142
if (
136-
this.flags & SubscriberFlags.Recursed &&
137-
this.flags & EffectFlags.ALLOW_RECURSE
143+
flags & SubscriberFlags.Recursed &&
144+
flags & EffectFlags.ALLOW_RECURSE
138145
) {
139-
this.flags &= ~SubscriberFlags.Recursed
146+
this.flags = flags & ~SubscriberFlags.Recursed
140147
this.notify()
141148
}
142149
}
@@ -149,16 +156,22 @@ export class ReactiveEffect<T = any> implements ReactiveEffectOptions {
149156
cleanupEffect(this)
150157
this.onStop && this.onStop()
151158
this.flags |= EffectFlags.STOP
159+
160+
if (this.subs !== undefined) {
161+
unlink(this.subs)
162+
}
152163
}
153164
}
154165

155166
get dirty(): boolean {
156167
const flags = this.flags
157-
if (
158-
flags & SubscriberFlags.Dirty ||
159-
(flags & SubscriberFlags.PendingComputed && updateDirtyFlag(this, flags))
160-
) {
161-
return true
168+
if (flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) {
169+
if (flags & SubscriberFlags.Dirty || checkDirty(this.deps!)) {
170+
this.flags = flags | SubscriberFlags.Dirty
171+
return true
172+
} else {
173+
this.flags = flags & ~SubscriberFlags.Pending
174+
}
162175
}
163176
return false
164177
}

0 commit comments

Comments
 (0)
Please sign in to comment.