Skip to content

Commit 4997582

Browse files
fix: notify inner effects in the same order as non-inner effects (#87)
1 parent 3610e91 commit 4997582

File tree

2 files changed

+64
-71
lines changed

2 files changed

+64
-71
lines changed

src/index.ts

Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { createReactiveSystem, type ReactiveFlags, type ReactiveNode } from './system.js';
22

3-
interface EffectScope extends ReactiveNode { }
4-
53
interface Effect extends ReactiveNode {
64
fn(): void;
75
}
@@ -16,7 +14,13 @@ interface Signal<T = any> extends ReactiveNode {
1614
pendingValue: T;
1715
}
1816

19-
const queuedEffects: (Effect | EffectScope | undefined)[] = [];
17+
let cycle = 0;
18+
let batchDepth = 0;
19+
let notifyIndex = 0;
20+
let queuedLength = 0;
21+
let activeSub: ReactiveNode | undefined;
22+
23+
const queued: (Effect | undefined)[] = [];
2024
const {
2125
link,
2226
unlink,
@@ -31,8 +35,29 @@ const {
3135
return updateSignal(node as Signal);
3236
}
3337
},
34-
notify,
35-
unwatched(node: Signal | Computed | Effect | EffectScope) {
38+
notify(effect: Effect) {
39+
let flags = effect.flags;
40+
let insertIndex = queuedLength;
41+
let firstInsertedIndex = insertIndex;
42+
43+
do {
44+
effect.flags = flags & ~(2 satisfies ReactiveFlags.Watching);
45+
queued[insertIndex++] = effect;
46+
effect = effect.subs?.sub as Effect;
47+
if (effect === undefined || !((flags = effect.flags) & 2 satisfies ReactiveFlags.Watching)) {
48+
break;
49+
}
50+
} while (true);
51+
52+
queuedLength = insertIndex;
53+
54+
while (firstInsertedIndex < insertIndex--) {
55+
const left = queued[firstInsertedIndex];
56+
queued[firstInsertedIndex++] = queued[insertIndex];
57+
queued[insertIndex] = left;
58+
}
59+
},
60+
unwatched(node) {
3661
if (!(node.flags & 1 satisfies ReactiveFlags.Mutable)) {
3762
effectScopeOper.call(node);
3863
} else if (node.depsTail !== undefined) {
@@ -43,12 +68,6 @@ const {
4368
},
4469
});
4570

46-
let cycle = 0;
47-
let batchDepth = 0;
48-
let notifyIndex = 0;
49-
let queuedEffectsLength = 0;
50-
let activeSub: ReactiveNode | undefined;
51-
5271
export function getActiveSub(): ReactiveNode | undefined {
5372
return activeSub;
5473
}
@@ -144,7 +163,7 @@ export function effect(fn: () => void): () => void {
144163
}
145164

146165
export function effectScope(fn: () => void): () => void {
147-
const e: EffectScope = {
166+
const e: ReactiveNode = {
148167
deps: undefined,
149168
depsTail: undefined,
150169
subs: undefined,
@@ -183,28 +202,13 @@ function updateSignal(s: Signal): boolean {
183202
return s.currentValue !== (s.currentValue = s.pendingValue);
184203
}
185204

186-
function notify(e: Effect | EffectScope) {
205+
function run(e: Effect): void {
187206
const flags = e.flags;
188-
if (!(flags & 64 /* Queued */)) {
189-
e.flags = flags | 64 /* Queued */;
190-
const subs = e.subs;
191-
if (subs !== undefined) {
192-
notify(subs.sub as Effect | EffectScope);
193-
} else {
194-
queuedEffects[queuedEffectsLength++] = e;
195-
}
196-
}
197-
}
198-
199-
function run(e: Effect | EffectScope, flags: ReactiveFlags): void {
200207
if (
201208
flags & 16 satisfies ReactiveFlags.Dirty
202209
|| (
203210
flags & 32 satisfies ReactiveFlags.Pending
204-
&& (
205-
checkDirty(e.deps!, e)
206-
|| (e.flags = flags & ~(32 satisfies ReactiveFlags.Pending), false)
207-
)
211+
&& checkDirty(e.deps!, e)
208212
)
209213
) {
210214
++cycle;
@@ -219,26 +223,18 @@ function run(e: Effect | EffectScope, flags: ReactiveFlags): void {
219223
purgeDeps(e);
220224
}
221225
} else {
222-
let link = e.deps;
223-
while (link !== undefined) {
224-
const dep = link.dep;
225-
const depFlags = dep.flags;
226-
if (depFlags & 64 /* Queued */) {
227-
run(dep, dep.flags = depFlags & ~(64 /* Queued */));
228-
}
229-
link = link.nextDep;
230-
}
226+
e.flags = 2 satisfies ReactiveFlags.Watching;
231227
}
232228
}
233229

234230
function flush(): void {
235-
while (notifyIndex < queuedEffectsLength) {
236-
const effect = queuedEffects[notifyIndex]!;
237-
queuedEffects[notifyIndex++] = undefined;
238-
run(effect, effect.flags &= ~(64 /* Queued */));
231+
while (notifyIndex < queuedLength) {
232+
const effect = queued[notifyIndex]!;
233+
queued[notifyIndex++] = undefined;
234+
run(effect);
239235
}
240236
notifyIndex = 0;
241-
queuedEffectsLength = 0;
237+
queuedLength = 0;
242238
}
243239

244240
function computedOper<T>(this: Computed<T>): T {
@@ -312,7 +308,7 @@ function effectOper(this: Effect): void {
312308
effectScopeOper.call(this);
313309
}
314310

315-
function effectScopeOper(this: EffectScope): void {
311+
function effectScopeOper(this: ReactiveNode): void {
316312
this.depsTail = undefined;
317313
this.flags = 0 satisfies ReactiveFlags.None;
318314
purgeDeps(this);

tests/effect.spec.ts

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -81,64 +81,61 @@ test('should not trigger inner effect when resolve maybe dirty', () => {
8181
a(2);
8282
});
8383

84-
test('should trigger inner effects in sequence', () => {
84+
test('should notify inner effects in the same order as non-inner effects', () => {
8585
const a = signal(0);
8686
const b = signal(0);
8787
const c = computed(() => a() - b());
88-
const order: string[] = [];
88+
const order1: string[] = [];
89+
const order2: string[] = [];
90+
const order3: string[] = [];
8991

9092
effect(() => {
91-
c();
93+
order1.push('effect1');
94+
a();
95+
});
96+
effect(() => {
97+
order1.push('effect2');
98+
a();
99+
b();
100+
});
92101

102+
effect(() => {
103+
c();
93104
effect(() => {
94-
order.push('first inner');
105+
order2.push('effect1');
95106
a();
96107
});
97-
98108
effect(() => {
99-
order.push('last inner');
109+
order2.push('effect2');
100110
a();
101111
b();
102112
});
103113
});
104114

105-
order.length = 0;
106-
107-
startBatch();
108-
b(1);
109-
a(1);
110-
endBatch();
111-
112-
expect(order).toEqual(['first inner', 'last inner']);
113-
});
114-
115-
test('should trigger inner effects in sequence in effect scope', () => {
116-
const a = signal(0);
117-
const b = signal(0);
118-
const order: string[] = [];
119-
120115
effectScope(() => {
121-
122116
effect(() => {
123-
order.push('first inner');
117+
order3.push('effect1');
124118
a();
125119
});
126-
127120
effect(() => {
128-
order.push('last inner');
121+
order3.push('effect2');
129122
a();
130123
b();
131124
});
132125
});
133126

134-
order.length = 0;
127+
order1.length = 0;
128+
order2.length = 0;
129+
order3.length = 0;
135130

136131
startBatch();
137132
b(1);
138133
a(1);
139134
endBatch();
140135

141-
expect(order).toEqual(['first inner', 'last inner']);
136+
expect(order1).toEqual(['effect2', 'effect1']);
137+
expect(order2).toEqual(order1);
138+
expect(order3).toEqual(order1);
142139
});
143140

144141
test('should custom effect support batch', () => {

0 commit comments

Comments
 (0)