Skip to content
This repository was archived by the owner on Dec 11, 2025. It is now read-only.

Commit 17d73cb

Browse files
feat: reactive system (#57)
Co-authored-by: Shinigami92 <[email protected]>
1 parent 828740f commit 17d73cb

File tree

6 files changed

+370
-21
lines changed

6 files changed

+370
-21
lines changed

.changeset/ninety-years-peel.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'unuse-reactivity': minor
3+
'unuse': minor
4+
'unuse-angular': minor
5+
'unuse-react': minor
6+
'unuse-solid': minor
7+
'unuse-vue': minor
8+
---
9+
10+
feat: reactive system

packages/unuse-reactivity/src/unComputed/index.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,42 @@
1-
import { computed } from 'alien-signals';
1+
import type { ReactiveFlags, ReactiveNode } from 'alien-signals';
2+
import {
3+
checkDirty,
4+
link,
5+
REACTIVITY_STATE,
6+
shallowPropagate,
7+
updateComputed,
8+
} from '../unReactiveSystem';
9+
10+
export interface UnComputedState<T> extends ReactiveNode {
11+
value: T | undefined;
12+
getter: (previousValue?: T) => T;
13+
}
14+
15+
export function computedOper<T>(this: UnComputedState<T>): T {
16+
const flags = this.flags;
17+
if (
18+
flags & (16 satisfies ReactiveFlags.Dirty) ||
19+
(flags & (32 satisfies ReactiveFlags.Pending) &&
20+
checkDirty(this.deps!, this))
21+
) {
22+
if (updateComputed(this)) {
23+
const subs = this.subs;
24+
if (subs !== undefined) {
25+
shallowPropagate(subs);
26+
}
27+
}
28+
} else if (flags & (32 satisfies ReactiveFlags.Pending)) {
29+
this.flags = flags & ~(32 satisfies ReactiveFlags.Pending);
30+
}
31+
32+
if (REACTIVITY_STATE.activeSub !== undefined) {
33+
link(this, REACTIVITY_STATE.activeSub);
34+
} else if (REACTIVITY_STATE.activeScope !== undefined) {
35+
link(this, REACTIVITY_STATE.activeScope);
36+
}
37+
38+
return this.value!;
39+
}
240

341
/**
442
* A unique symbol used to identify `UnComputed` objects.
@@ -30,14 +68,19 @@ export interface UnComputed<T> {
3068
* @returns An `UnComputed` object that has a `get` method to retrieve the current value.
3169
*/
3270
export function unComputed<T>(callback: () => T): UnComputed<T> {
33-
const value = computed(callback);
71+
const get: UnComputed<T>['get'] = computedOper.bind({
72+
value: undefined,
73+
subs: undefined,
74+
subsTail: undefined,
75+
deps: undefined,
76+
depsTail: undefined,
77+
flags: 17 as ReactiveFlags.Mutable | ReactiveFlags.Dirty,
78+
getter: callback,
79+
} satisfies UnComputedState<T>) as UnComputed<T>['get'];
3480

3581
return {
3682
[UN_COMPUTED]: true,
37-
get() {
38-
// We need to call the computed inside the getter to ensure effects are triggered
39-
return value();
40-
},
83+
get,
4184
};
4285
}
4386

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,39 @@
1-
import { effect } from 'alien-signals';
1+
import type { ReactiveFlags, ReactiveNode } from 'alien-signals';
2+
import {
3+
effectOper,
4+
link,
5+
REACTIVITY_STATE,
6+
setCurrentSub,
7+
} from '../unReactiveSystem';
8+
9+
export interface UnEffectState extends ReactiveNode {
10+
fn(): void;
11+
}
212

313
export type UnEffectReturn = () => void;
414

515
export function unEffect(callback: () => void): UnEffectReturn {
6-
return effect(callback);
16+
const state: UnEffectState = {
17+
fn: callback,
18+
subs: undefined,
19+
subsTail: undefined,
20+
deps: undefined,
21+
depsTail: undefined,
22+
flags: 2 satisfies ReactiveFlags.Watching,
23+
};
24+
25+
if (REACTIVITY_STATE.activeSub !== undefined) {
26+
link(state, REACTIVITY_STATE.activeSub);
27+
} else if (REACTIVITY_STATE.activeScope !== undefined) {
28+
link(state, REACTIVITY_STATE.activeScope);
29+
}
30+
31+
const prev = setCurrentSub(state);
32+
try {
33+
state.fn();
34+
} finally {
35+
setCurrentSub(prev);
36+
}
37+
38+
return effectOper.bind(state) as UnEffectReturn;
739
}
Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,37 @@
1-
import { effectScope } from 'alien-signals';
1+
import type { ReactiveFlags, ReactiveNode } from 'alien-signals';
2+
import {
3+
effectOper,
4+
link,
5+
REACTIVITY_STATE,
6+
setCurrentScope,
7+
setCurrentSub,
8+
} from '../unReactiveSystem';
9+
10+
export type UnEffectScopeState = ReactiveNode;
211

312
export type UnEffectScopeReturn = () => void;
413

514
export function unEffectScope(callback: () => void): UnEffectScopeReturn {
6-
return effectScope(callback);
15+
const state: UnEffectScopeState = {
16+
deps: undefined,
17+
depsTail: undefined,
18+
subs: undefined,
19+
subsTail: undefined,
20+
flags: 0 satisfies ReactiveFlags.None,
21+
};
22+
23+
if (REACTIVITY_STATE.activeScope !== undefined) {
24+
link(state, REACTIVITY_STATE.activeScope);
25+
}
26+
27+
const prevSub = setCurrentSub(undefined);
28+
const prevScope = setCurrentScope(state);
29+
try {
30+
callback();
31+
} finally {
32+
setCurrentScope(prevScope);
33+
setCurrentSub(prevSub);
34+
}
35+
36+
return effectOper.bind(state) as UnEffectScopeReturn;
737
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import type { ReactiveFlags, ReactiveNode } from 'alien-signals/system';
2+
import { createReactiveSystem } from 'alien-signals/system';
3+
import type { UnComputedState } from '../unComputed';
4+
import type { UnEffectState } from '../unEffect';
5+
import type { UnEffectScopeState } from '../unEffectScope';
6+
import type { UnSignalState } from '../unSignal';
7+
8+
export const Queued = 1 << 6;
9+
10+
export const queuedEffects: Array<
11+
UnEffectState | UnEffectScopeState | undefined
12+
> = [];
13+
export const {
14+
link,
15+
unlink,
16+
propagate,
17+
checkDirty,
18+
endTracking,
19+
startTracking,
20+
shallowPropagate,
21+
} = createReactiveSystem({
22+
update(signal: UnSignalState<unknown> | UnComputedState<unknown>): boolean {
23+
if ('getter' in signal) {
24+
return updateComputed(signal);
25+
}
26+
27+
return updateSignal(signal, signal.value);
28+
},
29+
notify,
30+
unwatched(
31+
node:
32+
| UnSignalState<unknown>
33+
| UnComputedState<unknown>
34+
| UnEffectState
35+
| UnEffectScopeState
36+
) {
37+
if ('getter' in node) {
38+
let toRemove = node.deps;
39+
if (toRemove !== undefined) {
40+
node.flags = 17 as ReactiveFlags.Mutable | ReactiveFlags.Dirty;
41+
do {
42+
toRemove = unlink(toRemove, node);
43+
} while (toRemove !== undefined);
44+
}
45+
} else if (!('previousValue' in node)) {
46+
effectOper.call(node);
47+
}
48+
},
49+
});
50+
51+
export interface UnReactivityState {
52+
batchDepth: number;
53+
notifyIndex: number;
54+
queuedEffectsLength: number;
55+
activeSub: ReactiveNode | undefined;
56+
activeScope: UnEffectScopeState | undefined;
57+
}
58+
59+
export const REACTIVITY_STATE: UnReactivityState = {
60+
batchDepth: 0,
61+
notifyIndex: 0,
62+
queuedEffectsLength: 0,
63+
activeSub: undefined,
64+
activeScope: undefined,
65+
};
66+
67+
export function getCurrentSub(): ReactiveNode | undefined {
68+
return REACTIVITY_STATE.activeSub;
69+
}
70+
71+
export function setCurrentSub(
72+
sub: ReactiveNode | undefined
73+
): ReactiveNode | undefined {
74+
const prevSub = REACTIVITY_STATE.activeSub;
75+
REACTIVITY_STATE.activeSub = sub;
76+
return prevSub;
77+
}
78+
79+
export function getCurrentScope(): UnEffectScopeState | undefined {
80+
return REACTIVITY_STATE.activeScope;
81+
}
82+
83+
export function setCurrentScope(
84+
scope: UnEffectScopeState | undefined
85+
): UnEffectScopeState | undefined {
86+
const prevScope = REACTIVITY_STATE.activeScope;
87+
REACTIVITY_STATE.activeScope = scope;
88+
return prevScope;
89+
}
90+
91+
export function getBatchDepth(): number {
92+
return REACTIVITY_STATE.batchDepth;
93+
}
94+
95+
export function startBatch(): void {
96+
++REACTIVITY_STATE.batchDepth;
97+
}
98+
99+
export function endBatch(): void {
100+
if (!--REACTIVITY_STATE.batchDepth) {
101+
flush();
102+
}
103+
}
104+
105+
export function updateSignal<T>(s: UnSignalState<T>, value: T): boolean {
106+
s.flags = 1 satisfies ReactiveFlags.Mutable;
107+
return s.previousValue !== (s.previousValue = value);
108+
}
109+
110+
export function updateComputed<T>(c: UnComputedState<T>): boolean {
111+
const prevSub = setCurrentSub(c);
112+
startTracking(c);
113+
try {
114+
const oldValue = c.value;
115+
return oldValue !== (c.value = c.getter(oldValue));
116+
} finally {
117+
setCurrentSub(prevSub);
118+
endTracking(c);
119+
}
120+
}
121+
122+
export function notify(e: UnEffectState | UnEffectScopeState): void {
123+
const flags = e.flags;
124+
if (!(flags & Queued)) {
125+
e.flags = flags | Queued;
126+
const subs = e.subs;
127+
if (subs === undefined) {
128+
queuedEffects[REACTIVITY_STATE.queuedEffectsLength++] = e;
129+
} else {
130+
notify(subs.sub);
131+
}
132+
}
133+
}
134+
135+
export function run(
136+
e: UnEffectState | UnEffectScopeState,
137+
flags: ReactiveFlags
138+
): void {
139+
if (
140+
flags & (16 satisfies ReactiveFlags.Dirty) ||
141+
(flags & (32 satisfies ReactiveFlags.Pending) && checkDirty(e.deps!, e))
142+
) {
143+
const prev = setCurrentSub(e);
144+
startTracking(e);
145+
try {
146+
(e as UnEffectState).fn();
147+
} finally {
148+
setCurrentSub(prev);
149+
endTracking(e);
150+
}
151+
152+
return;
153+
} else if (flags & (32 satisfies ReactiveFlags.Pending)) {
154+
e.flags = flags & ~(32 satisfies ReactiveFlags.Pending);
155+
}
156+
157+
let link = e.deps;
158+
while (link !== undefined) {
159+
const dep = link.dep;
160+
const depFlags = dep.flags;
161+
if (depFlags & Queued) {
162+
run(dep, (dep.flags = depFlags & ~Queued));
163+
}
164+
165+
link = link.nextDep;
166+
}
167+
}
168+
169+
export function flush(): void {
170+
while (REACTIVITY_STATE.notifyIndex < REACTIVITY_STATE.queuedEffectsLength) {
171+
const effect = queuedEffects[REACTIVITY_STATE.notifyIndex]!;
172+
queuedEffects[REACTIVITY_STATE.notifyIndex++] = undefined;
173+
run(effect, (effect.flags &= ~Queued));
174+
}
175+
176+
REACTIVITY_STATE.notifyIndex = 0;
177+
REACTIVITY_STATE.queuedEffectsLength = 0;
178+
}
179+
180+
export function effectOper(this: UnEffectState | UnEffectScopeState): void {
181+
let dep = this.deps;
182+
while (dep !== undefined) {
183+
dep = unlink(dep, this);
184+
}
185+
186+
const sub = this.subs;
187+
if (sub !== undefined) {
188+
unlink(sub);
189+
}
190+
191+
this.flags = 0 satisfies ReactiveFlags.None;
192+
}

0 commit comments

Comments
 (0)