Skip to content

Commit 6cefbc4

Browse files
committed
feat: reimplementing effect scope
1 parent 600a95c commit 6cefbc4

3 files changed

Lines changed: 135 additions & 6 deletions

File tree

src/index.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './system.js';
33
import { createReactiveSystem, Dependency, Subscriber, SubscriberFlags } from './system.js';
44

55
interface Effect extends Subscriber, Dependency {
6+
isScope?: true;
67
fn(): void;
78
}
89

@@ -49,10 +50,13 @@ const {
4950
notifyEffect(e: Effect) {
5051
const flags = e.flags;
5152
if (
52-
flags & SubscriberFlags.Dirty
53-
|| (flags & SubscriberFlags.PendingComputed && updateDirtyFlag(e, flags))
53+
!e.isScope
54+
&& (
55+
flags & SubscriberFlags.Dirty
56+
|| (flags & SubscriberFlags.PendingComputed && updateDirtyFlag(e, flags))
57+
)
5458
) {
55-
executeEffect(e);
59+
runEffect(e);
5660
} else {
5761
processPendingInnerEffects(e, e.flags);
5862
}
@@ -63,6 +67,7 @@ const pauseStack: (Subscriber | undefined)[] = [];
6367

6468
let batchDepth = 0;
6569
let activeSub: Subscriber | undefined;
70+
let activeScope: Subscriber | undefined;
6671

6772
//#region Public functions
6873
export function startBatch() {
@@ -117,14 +122,33 @@ export function effect<T>(fn: () => T): () => void {
117122
};
118123
if (activeSub !== undefined) {
119124
link(e, activeSub);
125+
} else if (activeScope !== undefined) {
126+
link(e, activeScope);
120127
}
121-
executeEffect(e);
128+
runEffect(e);
129+
return effectStop.bind(e);
130+
}
131+
132+
export function effectScope<T>(fn: () => T): () => void {
133+
const e: Effect = {
134+
fn,
135+
subs: undefined,
136+
subsTail: undefined,
137+
deps: undefined,
138+
depsTail: undefined,
139+
flags: SubscriberFlags.Effect,
140+
isScope: true,
141+
};
142+
if (activeSub !== undefined) {
143+
link(e, activeSub);
144+
}
145+
runEffectScope(e);
122146
return effectStop.bind(e);
123147
}
124148
//#endregion
125149

126150
//#region Internal functions
127-
function executeEffect(e: Effect): void {
151+
function runEffect(e: Effect): void {
128152
const prevSub = activeSub;
129153
activeSub = e;
130154
startTracking(e);
@@ -135,6 +159,18 @@ function executeEffect(e: Effect): void {
135159
endTracking(e);
136160
}
137161
}
162+
163+
function runEffectScope(e: Effect): void {
164+
const prevSub = activeScope;
165+
activeScope = e;
166+
startTracking(e);
167+
try {
168+
e.fn();
169+
} finally {
170+
activeScope = prevSub;
171+
endTracking(e);
172+
}
173+
}
138174
//#endregion
139175

140176
//#region Bound functions
@@ -145,6 +181,8 @@ function computedGetter<T>(this: Computed<T>): T {
145181
}
146182
if (activeSub !== undefined) {
147183
link(this, activeSub);
184+
} else if (activeScope !== undefined) {
185+
link(this, activeScope);
148186
}
149187
return this.currentValue!;
150188
}

tests/effect.spec.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from 'vitest';
2-
import { computed, effect, endBatch, signal, startBatch } from '../src';
2+
import { computed, effect, effectScope, endBatch, signal, startBatch } from '../src';
33

44
test('should clear subscriptions when untracked by all subscribers', () => {
55
let bRunTimes = 0;
@@ -115,3 +115,71 @@ test('should trigger inner effects in sequence', () => {
115115

116116
expect(order).toEqual(['first inner', 'last inner']);
117117
});
118+
119+
test('should trigger inner effects in sequence in effect scope', () => {
120+
const a = signal(0);
121+
const b = signal(0);
122+
const order: string[] = [];
123+
124+
effectScope(() => {
125+
126+
effect(() => {
127+
order.push('first inner');
128+
a();
129+
});
130+
131+
effect(() => {
132+
order.push('last inner');
133+
a();
134+
b();
135+
});
136+
});
137+
138+
order.length = 0;
139+
140+
startBatch();
141+
b(1);
142+
a(1);
143+
endBatch();
144+
145+
expect(order).toEqual(['first inner', 'last inner']);
146+
});
147+
148+
test('should custom effect support batch', () => {
149+
function batchEffect(fn: () => void) {
150+
return effect(() => {
151+
startBatch();
152+
try {
153+
return fn();
154+
} finally {
155+
endBatch();
156+
}
157+
});
158+
}
159+
160+
const logs: string[] = [];
161+
const a = signal(0);
162+
const b = signal(0);
163+
164+
const aa = computed(() => {
165+
logs.push('aa-0');
166+
if (a() === 0) {
167+
b(1);
168+
}
169+
logs.push('aa-1');
170+
});
171+
172+
const bb = computed(() => {
173+
logs.push('bb');
174+
return b();
175+
});
176+
177+
batchEffect(() => {
178+
bb();
179+
});
180+
batchEffect(() => {
181+
aa();
182+
});
183+
184+
expect(logs).toEqual(['bb', 'aa-0', 'aa-1', 'bb']);
185+
});

tests/effectScope.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { expect, test } from 'vitest';
2+
import { effect, effectScope, signal } from '../src';
3+
4+
test('should not trigger after stop', () => {
5+
const count = signal(1);
6+
7+
let triggers = 0;
8+
let effect1;
9+
10+
const stopScope = effectScope(() => {
11+
effect1 = effect(() => {
12+
triggers++;
13+
count();
14+
});
15+
});
16+
17+
expect(triggers).toBe(1);
18+
count(2);
19+
expect(triggers).toBe(2);
20+
stopScope();
21+
count(3);
22+
expect(triggers).toBe(2);
23+
});

0 commit comments

Comments
 (0)