balises 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +222 -6
- package/dist/balises.esm.js +133 -21
- package/dist/balises.esm.js.map +1 -1
- package/dist/balises.iife.js +134 -20
- package/dist/balises.iife.js.map +1 -1
- package/dist/balises.iife.min.js +2 -2
- package/dist/balises.iife.min.js.map +1 -1
- package/dist/esm/signals/computed.d.ts +2 -0
- package/dist/esm/signals/computed.d.ts.map +1 -1
- package/dist/esm/signals/computed.js +55 -22
- package/dist/esm/signals/computed.js.map +1 -1
- package/dist/esm/signals/context.d.ts +23 -0
- package/dist/esm/signals/context.d.ts.map +1 -1
- package/dist/esm/signals/context.js +60 -5
- package/dist/esm/signals/context.js.map +1 -1
- package/dist/esm/signals/effect.d.ts +21 -0
- package/dist/esm/signals/effect.d.ts.map +1 -0
- package/dist/esm/signals/effect.js +40 -0
- package/dist/esm/signals/effect.js.map +1 -0
- package/dist/esm/signals/index.d.ts +2 -1
- package/dist/esm/signals/index.d.ts.map +1 -1
- package/dist/esm/signals/index.js +2 -1
- package/dist/esm/signals/index.js.map +1 -1
- package/dist/esm/signals/signal.d.ts +12 -0
- package/dist/esm/signals/signal.d.ts.map +1 -1
- package/dist/esm/signals/signal.js +15 -1
- package/dist/esm/signals/signal.js.map +1 -1
- package/package.json +19 -5
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<img alt="balises" src="./assets/logo.svg" width="280">
|
|
5
5
|
</picture>
|
|
6
6
|
|
|
7
|
-
### A minimal reactive HTML templating library. ~
|
|
7
|
+
### A minimal reactive HTML templating library. ~3.0KB gzipped.
|
|
8
8
|
|
|
9
9
|
**[📚️ Documentation & Examples](https://elbywan.github.io/balises/)**
|
|
10
10
|
|
|
@@ -28,7 +28,9 @@ import { html, signal } from "balises";
|
|
|
28
28
|
const count = signal(0);
|
|
29
29
|
|
|
30
30
|
const { fragment, dispose } = html`
|
|
31
|
-
<button @click=${() => count.
|
|
31
|
+
<button @click=${() => count.update((n) => n + 1)}>
|
|
32
|
+
Clicked ${count} times
|
|
33
|
+
</button>
|
|
32
34
|
`.render();
|
|
33
35
|
|
|
34
36
|
document.body.appendChild(fragment);
|
|
@@ -132,6 +134,20 @@ console.log(name.value); // "world"
|
|
|
132
134
|
name.value = "everyone"; // Notifies subscribers
|
|
133
135
|
```
|
|
134
136
|
|
|
137
|
+
**Updating based on current value:**
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const count = signal(0);
|
|
141
|
+
|
|
142
|
+
// Using update() for functional updates
|
|
143
|
+
count.update((n) => n + 1);
|
|
144
|
+
count.update((n) => n * 2);
|
|
145
|
+
|
|
146
|
+
// Equivalent to:
|
|
147
|
+
count.value = count.value + 1;
|
|
148
|
+
count.value = count.value * 2;
|
|
149
|
+
```
|
|
150
|
+
|
|
135
151
|
### `computed<T>(fn)` / `new Computed<T>(fn)`
|
|
136
152
|
|
|
137
153
|
Creates a derived value that auto-tracks dependencies.
|
|
@@ -148,6 +164,47 @@ console.log(fullName.value); // "Jane Doe"
|
|
|
148
164
|
|
|
149
165
|
Computeds are lazy - they only recompute when accessed and when their dependencies have changed.
|
|
150
166
|
|
|
167
|
+
### `effect(fn)`
|
|
168
|
+
|
|
169
|
+
Creates a side effect that automatically re-runs when its dependencies change. Unlike `computed()`, effects run immediately and are intended for side effects like DOM updates, logging, or persistence.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { signal, effect } from "balises";
|
|
173
|
+
|
|
174
|
+
const count = signal(0);
|
|
175
|
+
|
|
176
|
+
// Runs immediately, then whenever count changes
|
|
177
|
+
const dispose = effect(() => {
|
|
178
|
+
console.log("Count is now:", count.value);
|
|
179
|
+
document.title = `Count: ${count.value}`;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
count.value = 1; // Logs "Count is now: 1" and updates title
|
|
183
|
+
dispose(); // Stop the effect
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Use cases:**
|
|
187
|
+
|
|
188
|
+
- Syncing state to localStorage
|
|
189
|
+
- Updating document.title or other DOM properties
|
|
190
|
+
- Logging and analytics
|
|
191
|
+
- Network requests triggered by state changes
|
|
192
|
+
|
|
193
|
+
Effects are automatically disposed when the component that created them is disposed (via the template's `dispose()` function).
|
|
194
|
+
|
|
195
|
+
**Example: Auto-sync to localStorage**
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
const favorites = signal([]);
|
|
199
|
+
|
|
200
|
+
effect(() => {
|
|
201
|
+
localStorage.setItem("favorites", JSON.stringify(favorites.value));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// localStorage automatically updates whenever favorites changes
|
|
205
|
+
favorites.value = [...favorites.value, "new item"];
|
|
206
|
+
```
|
|
207
|
+
|
|
151
208
|
### `store<T>(obj)`
|
|
152
209
|
|
|
153
210
|
Proxy-based reactive wrapper. Nested plain objects are wrapped recursively.
|
|
@@ -158,6 +215,23 @@ state.count++; // Reactive
|
|
|
158
215
|
state.user.name = "Bob"; // Also reactive (nested)
|
|
159
216
|
```
|
|
160
217
|
|
|
218
|
+
**Note:** Array mutations like `push()`, `pop()`, `splice()` do **not** trigger reactivity. To update arrays reactively, reassign them:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const state = store({ items: [1, 2, 3] });
|
|
222
|
+
|
|
223
|
+
// ❌ Does NOT trigger reactivity
|
|
224
|
+
state.items.push(4);
|
|
225
|
+
|
|
226
|
+
// ✅ Triggers reactivity
|
|
227
|
+
state.items = [...state.items, 4];
|
|
228
|
+
|
|
229
|
+
// Alternative: Use signal for arrays if you want .update() method
|
|
230
|
+
const items = signal([1, 2, 3]);
|
|
231
|
+
items.update((arr) => [...arr, 4]);
|
|
232
|
+
items.update((arr) => arr.filter((n) => n !== 2));
|
|
233
|
+
```
|
|
234
|
+
|
|
161
235
|
### `batch<T>(fn)`
|
|
162
236
|
|
|
163
237
|
Batch multiple signal updates to defer subscriber notifications until the batch completes.
|
|
@@ -174,6 +248,28 @@ batch(() => {
|
|
|
174
248
|
}); // Subscribers notified once after both updates
|
|
175
249
|
```
|
|
176
250
|
|
|
251
|
+
### `scope(fn)`
|
|
252
|
+
|
|
253
|
+
Create a disposal scope that automatically collects all computeds and effects created within, allowing cleanup with a single `dispose()` call.
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { scope, signal, computed, effect } from "balises";
|
|
257
|
+
|
|
258
|
+
const [state, dispose] = scope(() => {
|
|
259
|
+
const count = signal(0);
|
|
260
|
+
const doubled = computed(() => count.value * 2);
|
|
261
|
+
effect(() => console.log(doubled.value));
|
|
262
|
+
return { count, doubled };
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Use state.count, state.doubled...
|
|
266
|
+
|
|
267
|
+
// Later: clean up everything at once
|
|
268
|
+
dispose();
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Useful for components, temporary reactive contexts, or any scenario where you want automatic cleanup of multiple reactive primitives.
|
|
272
|
+
|
|
177
273
|
### `isSignal(value)`
|
|
178
274
|
|
|
179
275
|
Type guard to check if a value is reactive (`Signal` or `Computed`).
|
|
@@ -212,17 +308,18 @@ doubled.dispose(); // Stops tracking, frees memory
|
|
|
212
308
|
The library supports granular imports for optimal bundle size:
|
|
213
309
|
|
|
214
310
|
```ts
|
|
215
|
-
// Full library (~
|
|
216
|
-
import { html, signal, computed } from "balises";
|
|
311
|
+
// Full library (~3.0KB gzipped)
|
|
312
|
+
import { html, signal, computed, effect } from "balises";
|
|
217
313
|
|
|
218
314
|
// Signals only
|
|
219
|
-
import { signal, computed, store, batch } from "balises/signals";
|
|
315
|
+
import { signal, computed, effect, store, batch, scope } from "balises/signals";
|
|
220
316
|
|
|
221
317
|
// Individual modules
|
|
222
318
|
import { signal } from "balises/signals/signal";
|
|
223
319
|
import { computed } from "balises/signals/computed";
|
|
320
|
+
import { effect } from "balises/signals/effect";
|
|
224
321
|
import { store } from "balises/signals/store";
|
|
225
|
-
import { batch } from "balises/signals/context";
|
|
322
|
+
import { batch, scope } from "balises/signals/context";
|
|
226
323
|
```
|
|
227
324
|
|
|
228
325
|
## Full Example
|
|
@@ -257,6 +354,125 @@ class Counter extends HTMLElement {
|
|
|
257
354
|
customElements.define("x-counter", Counter);
|
|
258
355
|
```
|
|
259
356
|
|
|
357
|
+
**With `signal.update()` for functional updates:**
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
import { html, signal, effect } from "balises";
|
|
361
|
+
|
|
362
|
+
class Counter extends HTMLElement {
|
|
363
|
+
#count = signal(0);
|
|
364
|
+
#dispose?: () => void;
|
|
365
|
+
|
|
366
|
+
connectedCallback() {
|
|
367
|
+
// Auto-sync to localStorage
|
|
368
|
+
const syncEffect = effect(() => {
|
|
369
|
+
localStorage.setItem("counter", String(this.#count.value));
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const { fragment, dispose } = html`
|
|
373
|
+
<div>
|
|
374
|
+
<p>Count: ${this.#count}</p>
|
|
375
|
+
<button @click=${() => this.#count.update((n) => n - 1)}>-</button>
|
|
376
|
+
<button @click=${() => this.#count.update((n) => n + 1)}>+</button>
|
|
377
|
+
</div>
|
|
378
|
+
`.render();
|
|
379
|
+
|
|
380
|
+
this.appendChild(fragment);
|
|
381
|
+
this.#dispose = () => {
|
|
382
|
+
syncEffect();
|
|
383
|
+
dispose();
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
disconnectedCallback() {
|
|
388
|
+
this.#dispose?.();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
<!-- BENCHMARK_RESULTS_START -->
|
|
394
|
+
|
|
395
|
+
## Benchmarks
|
|
396
|
+
|
|
397
|
+
Performance comparison of Balises against other popular reactive libraries. Benchmarks run in isolated processes to prevent V8 JIT contamination.
|
|
398
|
+
|
|
399
|
+
**Test Environment:**
|
|
400
|
+
|
|
401
|
+
- Node.js with V8 engine
|
|
402
|
+
- Each test runs in a separate process (isolated mode)
|
|
403
|
+
- **10 warmup runs** to stabilize JIT
|
|
404
|
+
- **100 iterations per test**, keeping middle 20 (discarding 40 best + 40 worst to reduce outliers)
|
|
405
|
+
- Tests measure pure reactive propagation (not DOM rendering)
|
|
406
|
+
|
|
407
|
+
**Scoring Methodology:**
|
|
408
|
+
|
|
409
|
+
- Overall ranking uses a combined score (50% average rank + 50% normalized average time)
|
|
410
|
+
- This ensures both consistency across scenarios (rank) and absolute performance (time) are valued equally
|
|
411
|
+
- "vs Fastest" compares average time to the fastest library
|
|
412
|
+
|
|
413
|
+
### Overall Performance
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
┌───────┬───────────────────┬──────────┬───────────────┬──────────────────┐
|
|
417
|
+
│ Rank │ Library │ Avg Rank │ Avg Time (μs) │ vs Fastest │
|
|
418
|
+
├───────┼───────────────────┼──────────┼───────────────┼──────────────────┤
|
|
419
|
+
│ #1 🏆 │ balises@0.2.1 │ 1.5 │ 69.76 │ 1.00x (baseline) │
|
|
420
|
+
├───────┼───────────────────┼──────────┼───────────────┼──────────────────┤
|
|
421
|
+
│ #2 │ preact@1.12.1 │ 1.7 │ 51.59 │ 0.74x │
|
|
422
|
+
├───────┼───────────────────┼──────────┼───────────────┼──────────────────┤
|
|
423
|
+
│ #3 │ vue@3.5.26 │ 3.0 │ 72.79 │ 1.04x │
|
|
424
|
+
├───────┼───────────────────┼──────────┼───────────────┼──────────────────┤
|
|
425
|
+
│ #4 │ maverick@6.0.0 │ 3.8 │ 91.40 │ 1.31x │
|
|
426
|
+
├───────┼───────────────────┼──────────┼───────────────┼──────────────────┤
|
|
427
|
+
│ #5 │ solid@1.9.10 │ 5.3 │ 225.26 │ 3.23x │
|
|
428
|
+
├───────┼───────────────────┼──────────┼───────────────┼──────────────────┤
|
|
429
|
+
│ #6 │ mobx@6.15.0 │ 5.8 │ 719.17 │ 10.31x │
|
|
430
|
+
├───────┼───────────────────┼──────────┼───────────────┼──────────────────┤
|
|
431
|
+
│ #7 │ hyperactiv@0.11.3 │ 6.8 │ 833.03 │ 11.94x │
|
|
432
|
+
└───────┴───────────────────┴──────────┴───────────────┴──────────────────┘
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Performance by Scenario
|
|
436
|
+
|
|
437
|
+
```
|
|
438
|
+
┌───────────────────┬───────────────┬─────────────┬────────────────┬────────────────────┬─────────────┬──────────────┬──────────┐
|
|
439
|
+
│ Library │ S1: 1: Layers │ S2: 2: Wide │ S3: 3: Diamond │ S4: 4: Conditional │ S5: 5: List │ S6: 6: Batch │ Avg Rank │
|
|
440
|
+
├───────────────────┼───────────────┼─────────────┼────────────────┼────────────────────┼─────────────┼──────────────┼──────────┤
|
|
441
|
+
│ balises@0.2.1 │ #3 │ #1 🏆 │ #1 🏆 │ #2 │ #1 🏆 │ #1 🏆 │ 1.5 │
|
|
442
|
+
├───────────────────┼───────────────┼─────────────┼────────────────┼────────────────────┼─────────────┼──────────────┼──────────┤
|
|
443
|
+
│ preact@1.12.1 │ #1 🏆 │ #2 │ #2 │ #1 🏆 │ #2 │ #2 │ 1.7 │
|
|
444
|
+
├───────────────────┼───────────────┼─────────────┼────────────────┼────────────────────┼─────────────┼──────────────┼──────────┤
|
|
445
|
+
│ vue@3.5.26 │ #2 │ #3 │ #3 │ #3 │ #3 │ #4 │ 3.0 │
|
|
446
|
+
├───────────────────┼───────────────┼─────────────┼────────────────┼────────────────────┼─────────────┼──────────────┼──────────┤
|
|
447
|
+
│ maverick@6.0.0 │ #4 │ #4 │ #4 │ #4 │ #4 │ #3 │ 3.8 │
|
|
448
|
+
├───────────────────┼───────────────┼─────────────┼────────────────┼────────────────────┼─────────────┼──────────────┼──────────┤
|
|
449
|
+
│ solid@1.9.10 │ #5 │ #6 │ #5 │ #5 │ #6 │ #5 │ 5.3 │
|
|
450
|
+
├───────────────────┼───────────────┼─────────────┼────────────────┼────────────────────┼─────────────┼──────────────┼──────────┤
|
|
451
|
+
│ mobx@6.15.0 │ #7 │ #5 │ #6 │ #6 │ #5 │ #6 │ 5.8 │
|
|
452
|
+
├───────────────────┼───────────────┼─────────────┼────────────────┼────────────────────┼─────────────┼──────────────┼──────────┤
|
|
453
|
+
│ hyperactiv@0.11.3 │ #6 │ #7 │ #7 │ #7 │ #7 │ #7 │ 6.8 │
|
|
454
|
+
└───────────────────┴───────────────┴─────────────┴────────────────┴────────────────────┴─────────────┴──────────────┴──────────┘
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**Scenarios:**
|
|
458
|
+
|
|
459
|
+
- **S1: Layers** - Deep dependency chains (A→B→C→D...)
|
|
460
|
+
- **S2: Wide** - Many independent signals updating in parallel
|
|
461
|
+
- **S3: Diamond** - Multiple paths to same computed (diamond dependencies)
|
|
462
|
+
- **S4: Conditional** - Dynamic subscriptions (like v-if logic)
|
|
463
|
+
- **S5: List** - List operations with filtering (like v-for patterns)
|
|
464
|
+
- **S6: Batch** - Batched/transactional updates
|
|
465
|
+
|
|
466
|
+
**Interpretation:**
|
|
467
|
+
|
|
468
|
+
- Balises excels at diamond dependencies, list operations, and batching while maintaining competitive performance across all scenarios
|
|
469
|
+
- Results show pure reactivity performance - real-world apps should consider framework ecosystem, DX, and specific use cases
|
|
470
|
+
- Lower rank = better performance
|
|
471
|
+
|
|
472
|
+
_Last updated: 2025-12-30_
|
|
473
|
+
|
|
474
|
+
<!-- BENCHMARK_RESULTS_END -->
|
|
475
|
+
|
|
260
476
|
## Scripts
|
|
261
477
|
|
|
262
478
|
```bash
|
package/dist/balises.esm.js
CHANGED
|
@@ -14,14 +14,14 @@ let batchQueue = null;
|
|
|
14
14
|
*/
|
|
15
15
|
function batch(fn) {
|
|
16
16
|
batchDepth++;
|
|
17
|
-
if (batchDepth === 1) batchQueue =
|
|
17
|
+
if (batchDepth === 1) batchQueue = /* @__PURE__ */ new Set();
|
|
18
18
|
try {
|
|
19
19
|
return fn();
|
|
20
20
|
} finally {
|
|
21
21
|
if (--batchDepth === 0) {
|
|
22
22
|
const q = batchQueue;
|
|
23
23
|
batchQueue = null;
|
|
24
|
-
for (
|
|
24
|
+
for (const sub of q) sub();
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -31,11 +31,51 @@ function isBatching() {
|
|
|
31
31
|
}
|
|
32
32
|
/** Add a subscriber to the batch queue */
|
|
33
33
|
function enqueueBatch(sub) {
|
|
34
|
-
batchQueue.
|
|
34
|
+
batchQueue.add(sub);
|
|
35
35
|
}
|
|
36
36
|
/** Add multiple subscribers to the batch queue */
|
|
37
37
|
function enqueueBatchAll(subs) {
|
|
38
|
-
batchQueue.
|
|
38
|
+
for (let i = 0; i < subs.length; i++) batchQueue.add(subs[i]);
|
|
39
|
+
}
|
|
40
|
+
/** Scope disposal: collect all disposers in a scope */
|
|
41
|
+
let disposalStack = null;
|
|
42
|
+
/**
|
|
43
|
+
* Create a disposal scope that collects all subscriptions and computeds created within.
|
|
44
|
+
* Returns the result of the function and a dispose function that cleans up all resources.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* const [result, dispose] = scope(() => {
|
|
49
|
+
* const count = signal(0);
|
|
50
|
+
* const doubled = computed(() => count.value * 2);
|
|
51
|
+
* effect(() => console.log(doubled.value));
|
|
52
|
+
* return { count, doubled };
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* // Later: clean up all subscriptions and computeds
|
|
56
|
+
* dispose();
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
function scope(fn) {
|
|
60
|
+
const disposers = [];
|
|
61
|
+
if (!disposalStack) disposalStack = [];
|
|
62
|
+
disposalStack.push(disposers);
|
|
63
|
+
try {
|
|
64
|
+
return [fn(), () => {
|
|
65
|
+
for (let i = disposers.length - 1; i >= 0; i--) disposers[i]();
|
|
66
|
+
disposers.length = 0;
|
|
67
|
+
}];
|
|
68
|
+
} finally {
|
|
69
|
+
disposalStack.pop();
|
|
70
|
+
if (disposalStack.length === 0) disposalStack = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Register a disposer in the current scope (if any).
|
|
75
|
+
* This is called internally by computed/effect when they create cleanup functions.
|
|
76
|
+
*/
|
|
77
|
+
function registerDisposer(dispose) {
|
|
78
|
+
if (disposalStack && disposalStack.length > 0) disposalStack[disposalStack.length - 1].push(dispose);
|
|
39
79
|
}
|
|
40
80
|
|
|
41
81
|
//#endregion
|
|
@@ -53,6 +93,8 @@ function removeFromArray(array, item) {
|
|
|
53
93
|
/**
|
|
54
94
|
* A reactive value container. When the value changes, all dependent
|
|
55
95
|
* computeds are marked dirty and subscribers are notified.
|
|
96
|
+
*
|
|
97
|
+
* Uses Object.is() for equality checks to correctly handle NaN values.
|
|
56
98
|
*/
|
|
57
99
|
var Signal = class {
|
|
58
100
|
#value;
|
|
@@ -66,7 +108,7 @@ var Signal = class {
|
|
|
66
108
|
return this.#value;
|
|
67
109
|
}
|
|
68
110
|
set value(v) {
|
|
69
|
-
if (this.#value
|
|
111
|
+
if (Object.is(this.#value, v)) return;
|
|
70
112
|
this.#value = v;
|
|
71
113
|
const targets = this.#targets;
|
|
72
114
|
for (let i = 0; i < targets.length; i++) targets[i].markDirty();
|
|
@@ -77,6 +119,18 @@ var Signal = class {
|
|
|
77
119
|
this.#subs.push(fn);
|
|
78
120
|
return () => removeFromArray(this.#subs, fn);
|
|
79
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Update the signal value using an updater function.
|
|
124
|
+
*
|
|
125
|
+
* @param fn - Function that receives current value and returns new value
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* const count = signal(0);
|
|
129
|
+
* count.update(n => n + 1); // increment
|
|
130
|
+
*/
|
|
131
|
+
update(fn) {
|
|
132
|
+
this.value = fn(this.#value);
|
|
133
|
+
}
|
|
80
134
|
/** @internal */
|
|
81
135
|
get targets() {
|
|
82
136
|
return this.#targets;
|
|
@@ -97,6 +151,8 @@ const signal = (value) => new Signal(value);
|
|
|
97
151
|
/**
|
|
98
152
|
* A derived reactive value. Automatically tracks dependencies and
|
|
99
153
|
* recomputes when any dependency changes.
|
|
154
|
+
*
|
|
155
|
+
* Uses Object.is() for equality checks to correctly handle NaN values.
|
|
100
156
|
*/
|
|
101
157
|
var Computed = class {
|
|
102
158
|
#fn;
|
|
@@ -110,6 +166,7 @@ var Computed = class {
|
|
|
110
166
|
constructor(fn) {
|
|
111
167
|
this.#fn = fn;
|
|
112
168
|
this.#recompute();
|
|
169
|
+
registerDisposer(() => this.dispose());
|
|
113
170
|
}
|
|
114
171
|
get value() {
|
|
115
172
|
if (this.#dirty) this.#recompute();
|
|
@@ -123,7 +180,10 @@ var Computed = class {
|
|
|
123
180
|
dispose() {
|
|
124
181
|
this.#fn = void 0;
|
|
125
182
|
const sources = this.#sources;
|
|
126
|
-
for (let i = 0; i < sources.length; i++)
|
|
183
|
+
for (let i = 0; i < sources.length; i++) {
|
|
184
|
+
const source = sources[i];
|
|
185
|
+
if (source) source.deleteTarget(this);
|
|
186
|
+
}
|
|
127
187
|
this.#sources = [];
|
|
128
188
|
this.#subs.length = 0;
|
|
129
189
|
}
|
|
@@ -136,7 +196,10 @@ var Computed = class {
|
|
|
136
196
|
const idx = this.#sourceIndex++;
|
|
137
197
|
if (idx < sources.length) {
|
|
138
198
|
if (sources[idx] === source) return;
|
|
139
|
-
for (let i = idx; i < sources.length; i++)
|
|
199
|
+
for (let i = idx; i < sources.length; i++) {
|
|
200
|
+
const source$1 = sources[i];
|
|
201
|
+
if (source$1) source$1.deleteTarget(this);
|
|
202
|
+
}
|
|
140
203
|
sources.length = idx;
|
|
141
204
|
}
|
|
142
205
|
sources.push(source);
|
|
@@ -154,20 +217,32 @@ var Computed = class {
|
|
|
154
217
|
if (c.#dirty) continue;
|
|
155
218
|
c.#dirty = true;
|
|
156
219
|
const targets = c.#targets;
|
|
157
|
-
|
|
158
|
-
const target = targets[j];
|
|
159
|
-
if (!target.#dirty) queue.push(target);
|
|
160
|
-
}
|
|
161
|
-
if (c.#subs.length) {
|
|
220
|
+
if (c.#subs.length > 0 && targets.length > 0 && c.#fn && !isBatching()) {
|
|
162
221
|
const old = c.#value;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
222
|
+
c.#recompute();
|
|
223
|
+
if (!Object.is(c.#value, old)) {
|
|
224
|
+
for (let j = 0; j < targets.length; j++) {
|
|
225
|
+
const target = targets[j];
|
|
226
|
+
if (!target.#dirty) queue.push(target);
|
|
167
227
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
228
|
+
for (let j = 0; j < c.#subs.length; j++) c.#subs[j]();
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
for (let j = 0; j < targets.length; j++) {
|
|
232
|
+
const target = targets[j];
|
|
233
|
+
if (!target.#dirty) queue.push(target);
|
|
234
|
+
}
|
|
235
|
+
if (c.#subs.length) {
|
|
236
|
+
const old = c.#value;
|
|
237
|
+
const notify = () => {
|
|
238
|
+
if (c.#fn) {
|
|
239
|
+
c.#recompute();
|
|
240
|
+
if (!Object.is(c.#value, old)) for (let j = 0; j < c.#subs.length; j++) c.#subs[j]();
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
if (isBatching()) enqueueBatch(notify);
|
|
244
|
+
else notify();
|
|
245
|
+
}
|
|
171
246
|
}
|
|
172
247
|
}
|
|
173
248
|
}
|
|
@@ -193,7 +268,10 @@ var Computed = class {
|
|
|
193
268
|
const newLen = this.#sourceIndex;
|
|
194
269
|
if (newLen < prevLen) {
|
|
195
270
|
const sources = this.#sources;
|
|
196
|
-
for (let i = newLen; i < prevLen; i++)
|
|
271
|
+
for (let i = newLen; i < prevLen; i++) {
|
|
272
|
+
const source = sources[i];
|
|
273
|
+
if (source) source.deleteTarget(this);
|
|
274
|
+
}
|
|
197
275
|
sources.length = newLen;
|
|
198
276
|
}
|
|
199
277
|
this.#dirty = false;
|
|
@@ -204,6 +282,40 @@ var Computed = class {
|
|
|
204
282
|
/** Create a new computed from the given function. */
|
|
205
283
|
const computed = (fn) => new Computed(fn);
|
|
206
284
|
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/signals/effect.ts
|
|
287
|
+
/**
|
|
288
|
+
* Effect - Run side effects reactively.
|
|
289
|
+
*/
|
|
290
|
+
/**
|
|
291
|
+
* Create a reactive effect that automatically tracks dependencies
|
|
292
|
+
* and re-runs when they change.
|
|
293
|
+
*
|
|
294
|
+
* @param fn - The effect function to run
|
|
295
|
+
* @returns A dispose function to stop the effect
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* const count = signal(0);
|
|
299
|
+
* const dispose = effect(() => {
|
|
300
|
+
* console.log("Count is:", count.value);
|
|
301
|
+
* });
|
|
302
|
+
*
|
|
303
|
+
* count.value = 1; // logs: "Count is: 1"
|
|
304
|
+
* dispose(); // stop the effect
|
|
305
|
+
*/
|
|
306
|
+
function effect(fn) {
|
|
307
|
+
const c = computed(() => {
|
|
308
|
+
fn();
|
|
309
|
+
});
|
|
310
|
+
const unsub = c.subscribe(() => {});
|
|
311
|
+
const dispose = () => {
|
|
312
|
+
unsub();
|
|
313
|
+
c.dispose();
|
|
314
|
+
};
|
|
315
|
+
registerDisposer(dispose);
|
|
316
|
+
return dispose;
|
|
317
|
+
}
|
|
318
|
+
|
|
207
319
|
//#endregion
|
|
208
320
|
//#region src/signals/store.ts
|
|
209
321
|
/**
|
|
@@ -648,5 +760,5 @@ function each(list, keyFnOrRenderFn, renderFn) {
|
|
|
648
760
|
}
|
|
649
761
|
|
|
650
762
|
//#endregion
|
|
651
|
-
export { Computed, Signal, Template, batch, computed, each, html, isSignal, signal, store };
|
|
763
|
+
export { Computed, Signal, Template, batch, computed, each, effect, html, isSignal, scope, signal, store };
|
|
652
764
|
//# sourceMappingURL=balises.esm.js.map
|