Skip to content

Commit 80a6782

Browse files
committedMar 20, 2025·
fix: queuing effects in an array
close #48
1 parent 39cb156 commit 80a6782

File tree

2 files changed

+103
-32
lines changed

2 files changed

+103
-32
lines changed
 

‎src/system.ts

+6-32
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export interface Link {
1616
// Reused to link the previous stack in propagate
1717
prevSub: Link | undefined;
1818
nextSub: Link | undefined;
19-
// Reused to link the notify effect in queuedEffects
2019
nextDep: Link | undefined;
2120
}
2221

@@ -60,8 +59,7 @@ export function createReactiveSystem({
6059
*/
6160
notifyEffect(effect: Subscriber): boolean;
6261
}) {
63-
let queuedEffects: Subscriber | undefined;
64-
let queuedEffectsTail: Subscriber | undefined;
62+
const queuedEffects: Subscriber[] = [];
6563

6664
return {
6765
/**
@@ -146,22 +144,12 @@ export function createReactiveSystem({
146144
continue;
147145
}
148146
if (subFlags & SubscriberFlags.Effect) {
149-
if (queuedEffectsTail !== undefined) {
150-
queuedEffectsTail.depsTail!.nextDep = sub.deps;
151-
} else {
152-
queuedEffects = sub;
153-
}
154-
queuedEffectsTail = sub;
147+
queuedEffects.push(sub);
155148
}
156149
} else if (!(subFlags & (SubscriberFlags.Tracking | targetFlag))) {
157150
sub.flags = subFlags | targetFlag | SubscriberFlags.Notified;
158151
if ((subFlags & (SubscriberFlags.Effect | SubscriberFlags.Notified)) === SubscriberFlags.Effect) {
159-
if (queuedEffectsTail !== undefined) {
160-
queuedEffectsTail.depsTail!.nextDep = sub.deps;
161-
} else {
162-
queuedEffects = sub;
163-
}
164-
queuedEffectsTail = sub;
152+
queuedEffects.push(sub);
165153
}
166154
} else if (
167155
!(subFlags & targetFlag)
@@ -313,17 +301,8 @@ export function createReactiveSystem({
313301
* notifications may be triggered until fully handled.
314302
*/
315303
processEffectNotifications(): void {
316-
while (queuedEffects !== undefined) {
317-
const effect = queuedEffects;
318-
const depsTail = effect.depsTail!;
319-
const queuedNext = depsTail.nextDep;
320-
if (queuedNext !== undefined) {
321-
depsTail.nextDep = undefined;
322-
queuedEffects = queuedNext.sub;
323-
} else {
324-
queuedEffects = undefined;
325-
queuedEffectsTail = undefined;
326-
}
304+
while (queuedEffects.length) {
305+
const effect = queuedEffects.shift()!;
327306
if (!notifyEffect(effect)) {
328307
effect.flags &= ~SubscriberFlags.Notified;
329308
}
@@ -474,12 +453,7 @@ export function createReactiveSystem({
474453
if ((subFlags & (SubscriberFlags.PendingComputed | SubscriberFlags.Dirty)) === SubscriberFlags.PendingComputed) {
475454
sub.flags = subFlags | SubscriberFlags.Dirty | SubscriberFlags.Notified;
476455
if ((subFlags & (SubscriberFlags.Effect | SubscriberFlags.Notified)) === SubscriberFlags.Effect) {
477-
if (queuedEffectsTail !== undefined) {
478-
queuedEffectsTail.depsTail!.nextDep = sub.deps;
479-
} else {
480-
queuedEffects = sub;
481-
}
482-
queuedEffectsTail = sub;
456+
queuedEffects.push(sub);
483457
}
484458
}
485459
link = link.nextSub!;

‎tests/issue_48.spec.ts

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { expect, test } from 'vitest';
2+
import { computed, effect, pauseTracking, resumeTracking, signal } from '../src';
3+
4+
test('#43', () => {
5+
const source = signal(0);
6+
let disposeInner: () => void;
7+
8+
reaction(
9+
() => source(),
10+
(val) => {
11+
if (val === 1) {
12+
disposeInner = reaction(
13+
() => source(),
14+
() => { }
15+
);
16+
} else if (val === 2) {
17+
disposeInner!();
18+
}
19+
}
20+
);
21+
22+
source(1);
23+
source(2);
24+
source(3);
25+
});
26+
27+
interface ReactionOptions<T = unknown, F extends boolean = boolean> {
28+
fireImmediately?: F;
29+
equals?: F extends true
30+
? (a: T, b: T | undefined) => boolean
31+
: (a: T, b: T) => boolean;
32+
onError?: (error: unknown) => void;
33+
scheduler?: (fn: () => void) => void;
34+
once?: boolean;
35+
}
36+
37+
function reaction<T>(
38+
dataFn: () => T,
39+
effectFn: (newValue: T, oldValue: T | undefined) => void,
40+
options: ReactionOptions<T> = {}
41+
): () => void {
42+
const {
43+
scheduler = (fn) => fn(),
44+
equals = Object.is,
45+
onError,
46+
once = false,
47+
fireImmediately = false,
48+
} = options;
49+
50+
let prevValue: T | undefined;
51+
let version = 0;
52+
53+
const tracked = computed(() => {
54+
try {
55+
return dataFn();
56+
} catch (error) {
57+
untracked(() => onError?.(error));
58+
return prevValue!;
59+
}
60+
});
61+
62+
const dispose = effect(() => {
63+
const current = tracked();
64+
if (!fireImmediately && version === 0) {
65+
prevValue = current;
66+
}
67+
version++;
68+
if (equals(current, prevValue!)) return;
69+
const oldValue = prevValue;
70+
prevValue = current;
71+
untracked(() =>
72+
scheduler(() => {
73+
try {
74+
effectFn(current, oldValue);
75+
} catch (error) {
76+
onError?.(error);
77+
} finally {
78+
if (once) {
79+
if (fireImmediately && version > 1) dispose();
80+
else if (!fireImmediately && version > 0) dispose();
81+
}
82+
}
83+
})
84+
);
85+
});
86+
87+
return dispose;
88+
}
89+
90+
function untracked<T>(callback: () => T): T {
91+
try {
92+
pauseTracking();
93+
return callback();
94+
} finally {
95+
resumeTracking();
96+
}
97+
}

0 commit comments

Comments
 (0)
Please sign in to comment.