what-core 0.6.2 → 0.8.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/src/render.js CHANGED
@@ -2,11 +2,21 @@
2
2
  // Solid-style rendering: components run once, signals create individual DOM effects.
3
3
  // No VDOM diffing — direct DOM manipulation with surgical signal-driven updates.
4
4
 
5
- import { effect, untrack, createRoot, signal, __DEV__ } from './reactive.js';
5
+ import { effect, untrack, createRoot, _createItemScope, signal, __DEV__ } from './reactive.js';
6
6
  import { createDOM, disposeTree, getCurrentComponent, getComponentStack } from './dom.js';
7
-
8
7
  export { effect, untrack };
9
8
 
9
+ // --- Generic text insertion hook ---
10
+ // External text engines (e.g., what-text) register a callback here via
11
+ // _setTextInsertHook(). When null (default), zero cost — no module loaded,
12
+ // no branch taken. The hook receives (parentElement, textString) on every
13
+ // dynamic text insertion and update.
14
+ let _onTextInsert = null;
15
+
16
+ export function _setTextInsertHook(fn) {
17
+ _onTextInsert = typeof fn === 'function' ? fn : null;
18
+ }
19
+
10
20
  // --- _$createComponent(Component, props, children) ---
11
21
  // Internal compiler target for component instantiation. The compiler emits calls
12
22
  // to this function instead of h() — keeping h() out of compiled output entirely.
@@ -15,7 +25,13 @@ export { effect, untrack };
15
25
  export function _$createComponent(Component, props, children) {
16
26
  if (children && children.length > 0) {
17
27
  const mergedChildren = children.length === 1 ? children[0] : children;
18
- props = props ? { ...props, children: mergedChildren } : { children: mergedChildren };
28
+ // Mutate props in place when possible to avoid object spread allocation.
29
+ // Compiled output creates a fresh props object per call, so mutation is safe.
30
+ if (props) {
31
+ props.children = mergedChildren;
32
+ } else {
33
+ props = { children: mergedChildren };
34
+ }
19
35
  }
20
36
  // Build a VNode-like object and pass to createDOM which handles component execution
21
37
  return createDOM({ tag: Component, props: props || {}, children: children || [], key: null, _vnode: true });
@@ -88,14 +104,10 @@ function _$templateImpl(html) {
88
104
  if (tableInfo) {
89
105
  const t = document.createElement('template');
90
106
  t.innerHTML = tableInfo.wrap + trimmed + tableInfo.unwrap;
91
- // Navigate down through the wrapper to reach the actual element
92
- return () => {
93
- let node = t.content.firstChild;
94
- for (let i = 0; i < tableInfo.depth; i++) {
95
- node = node.firstChild;
96
- }
97
- return node.cloneNode(true);
98
- };
107
+ // Pre-navigate to the target element once avoids per-clone traversal.
108
+ let target = t.content.firstChild;
109
+ for (let i = 0; i < tableInfo.depth; i++) target = target.firstChild;
110
+ return () => target.cloneNode(true);
99
111
  }
100
112
 
101
113
  const t = document.createElement('template');
@@ -156,13 +168,58 @@ export function svgTemplate(html) {
156
168
 
157
169
  export function insert(parent, child, marker) {
158
170
  if (typeof child === 'function') {
159
- let current = null;
171
+ // Fast path: if the first evaluation returns a string/number, optimistically
172
+ // create a text node for direct updates. If the value type changes later
173
+ // (e.g., text -> vnode), fall back to full reconcileInsert.
174
+ const first = child();
175
+ const t = typeof first;
176
+ if (t === 'string' || t === 'number') {
177
+ const textNode = document.createTextNode(String(first));
178
+ const m = marker || null;
179
+ if (m) parent.insertBefore(textNode, m);
180
+ else parent.appendChild(textNode);
181
+ if (_onTextInsert) _onTextInsert(parent, String(first));
182
+ let current = textNode;
183
+ let isTextFastPath = true;
184
+ effect(() => {
185
+ const val = child();
186
+ const vt = typeof val;
187
+ if (isTextFastPath && (vt === 'string' || vt === 'number')) {
188
+ // Fast path: still text — update data directly (no allocations)
189
+ const str = String(val);
190
+ if (textNode.data !== str) textNode.data = str;
191
+ if (_onTextInsert) _onTextInsert(parent, str);
192
+ } else {
193
+ // Type changed — fall back to full reconcile
194
+ isTextFastPath = false;
195
+ current = reconcileInsert(parent, val, current, m);
196
+ }
197
+ });
198
+ return textNode;
199
+ }
200
+ // General path for non-text reactive children (first value was null/vnode/array)
201
+ let current = first != null ? reconcileInsert(parent, first, null, marker || null) : null;
160
202
  effect(() => {
161
203
  current = reconcileInsert(parent, child(), current, marker || null);
162
204
  });
163
205
  return current;
164
206
  }
165
207
 
208
+ // Static text: create text node directly, skip reconcileInsert overhead
209
+ if (typeof child === 'string' || typeof child === 'number') {
210
+ const textNode = document.createTextNode(String(child));
211
+ if (marker) parent.insertBefore(textNode, marker);
212
+ else parent.appendChild(textNode);
213
+ return textNode;
214
+ }
215
+
216
+ // Static DOM node: insert directly, skip reconcileInsert overhead
217
+ if (child != null && typeof child === 'object' && child.nodeType > 0) {
218
+ if (marker) parent.insertBefore(child, marker);
219
+ else parent.appendChild(child);
220
+ return child;
221
+ }
222
+
166
223
  return reconcileInsert(parent, child, null, marker || null);
167
224
  }
168
225
 
@@ -176,10 +233,12 @@ function isVNode(value) {
176
233
  return !!value && typeof value === 'object' && (value._vnode === true || 'tag' in value);
177
234
  }
178
235
 
236
+ // Check if parent is an SVG element. Cached typeof check avoids repeated lookups.
237
+ const _hasSVGElement = typeof SVGElement !== 'undefined';
179
238
  function isSvgParent(parent) {
180
- return typeof SVGElement !== 'undefined'
239
+ return _hasSVGElement
181
240
  && parent instanceof SVGElement
182
- && parent.tagName.toLowerCase() !== 'foreignobject';
241
+ && parent.tagName !== 'foreignObject';
183
242
  }
184
243
 
185
244
  function asNodeArray(value) {
@@ -252,10 +311,26 @@ function reconcileInsert(parent, value, current, marker) {
252
311
  if ((typeof value === 'string' || typeof value === 'number')
253
312
  && current && !Array.isArray(current) && current.nodeType === 3) {
254
313
  const text = String(value);
255
- if (current.textContent !== text) current.textContent = text;
314
+ if (current.data !== text) current.data = text;
256
315
  return current;
257
316
  }
258
317
 
318
+ // Fast path: single DOM node value with single current node — skip array allocations
319
+ if (typeof value === 'object' && value !== null && value.nodeType > 0 && !Array.isArray(value)) {
320
+ if (value === current) return current;
321
+ if (current && !Array.isArray(current) && current.nodeType > 0) {
322
+ // Replace single node with single node
323
+ if (current.parentNode === parent) {
324
+ disposeTree(current);
325
+ parent.replaceChild(value, current);
326
+ } else {
327
+ if (targetMarker) parent.insertBefore(value, targetMarker);
328
+ else parent.appendChild(value);
329
+ }
330
+ return value;
331
+ }
332
+ }
333
+
259
334
  const newNodes = valuesToNodes(value, parent, []);
260
335
  const oldNodes = asNodeArray(current);
261
336
 
@@ -263,10 +338,17 @@ function reconcileInsert(parent, value, current, marker) {
263
338
  return current;
264
339
  }
265
340
 
266
- const keep = new Set(newNodes);
341
+ // Remove old nodes not in the new set. For small arrays (typical case),
342
+ // linear scan is faster than Set allocation + hashing.
343
+ const newLen = newNodes.length;
267
344
  for (let i = 0; i < oldNodes.length; i++) {
268
345
  const oldNode = oldNodes[i];
269
- if (!keep.has(oldNode) && oldNode.parentNode === parent) {
346
+ if (oldNode.parentNode !== parent) continue;
347
+ let found = false;
348
+ for (let j = 0; j < newLen; j++) {
349
+ if (newNodes[j] === oldNode) { found = true; break; }
350
+ }
351
+ if (!found) {
270
352
  disposeTree(oldNode);
271
353
  parent.removeChild(oldNode);
272
354
  }
@@ -318,7 +400,9 @@ export function mapArray(source, mapFn, options) {
318
400
  } else {
319
401
  reconcileList(parent, endMarker, items, newItems, mappedNodes, disposeFns, mapFn);
320
402
  }
321
- items = newItems.slice();
403
+ // Save a snapshot of items for next diff. Use slice() to defend against
404
+ // in-place mutation, but skip for empty arrays (common clear case).
405
+ items = newItems.length > 0 ? newItems.slice() : newItems;
322
406
  });
323
407
 
324
408
  return endMarker;
@@ -330,13 +414,21 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
330
414
  const oldLen = oldItems.length;
331
415
 
332
416
  if (newLen === 0) {
333
- // Fast path: clear all — remove only this list's nodes, not all parent content
417
+ // Fast path: clear all — dispose reactive scopes first (handles effects/cleanups),
418
+ // then remove DOM nodes. createRoot disposal handles all tracked effects; we only
419
+ // need disposeTree for nodes with additional reactive bindings outside createRoot.
334
420
  if (oldLen > 0) {
335
421
  for (let i = 0; i < oldLen; i++) {
336
- disposeFns[i]?.();
337
- if (mappedNodes[i]?.parentNode === parent) {
338
- disposeTree(mappedNodes[i]);
339
- parent.removeChild(mappedNodes[i]);
422
+ if (disposeFns[i]) disposeFns[i]();
423
+ }
424
+ for (let i = oldLen - 1; i >= 0; i--) {
425
+ const node = mappedNodes[i];
426
+ if (node) {
427
+ // Only walk subtree if the node has reactive state not tracked by createRoot
428
+ if (node._componentCtx || node._dispose || node._propEffects) {
429
+ disposeTree(node);
430
+ }
431
+ if (node.parentNode === parent) parent.removeChild(node);
340
432
  }
341
433
  }
342
434
  mappedNodes.length = 0;
@@ -350,7 +442,7 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
350
442
  const frag = document.createDocumentFragment();
351
443
  for (let i = 0; i < newLen; i++) {
352
444
  const item = newItems[i];
353
- const node = createRoot(dispose => {
445
+ const node = _createItemScope(dispose => {
354
446
  disposeFns[i] = dispose;
355
447
  return mapFn(item, i);
356
448
  });
@@ -407,7 +499,7 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
407
499
  for (let i = start; i <= newEnd; i++) {
408
500
  const item = newItems[i];
409
501
  const idx = i;
410
- newMapped[i] = createRoot(dispose => {
502
+ newMapped[i] = _createItemScope(dispose => {
411
503
  newDispose[idx] = dispose;
412
504
  return mapFn(item, idx);
413
505
  });
@@ -492,7 +584,7 @@ function _reconcileMiddle(parent, endMarker, oldItems, newItems, mappedNodes, di
492
584
  if (!newMapped[i]) {
493
585
  const item = newItems[i];
494
586
  const idx = i;
495
- newMapped[i] = createRoot(dispose => {
587
+ newMapped[i] = _createItemScope(dispose => {
496
588
  newDispose[idx] = dispose;
497
589
  return mapFn(item, idx);
498
590
  });
@@ -571,13 +663,17 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
571
663
  // --- Fast path: clear all ---
572
664
  if (newLen === 0) {
573
665
  if (oldLen > 0) {
574
- // Call dispose functions to run cleanup callbacks (onCleanup, effect cleanups).
575
- // Without this, cleanup callbacks leak.
666
+ // Dispose reactive scopes first, then remove DOM nodes.
576
667
  for (let i = 0; i < oldLen; i++) {
577
- disposeFns[i]?.();
578
- if (mappedNodes[i]?.parentNode === parent) {
579
- disposeTree(mappedNodes[i]);
580
- parent.removeChild(mappedNodes[i]);
668
+ if (disposeFns[i]) disposeFns[i]();
669
+ }
670
+ for (let i = oldLen - 1; i >= 0; i--) {
671
+ const node = mappedNodes[i];
672
+ if (node) {
673
+ if (node._componentCtx || node._dispose || node._propEffects) {
674
+ disposeTree(node);
675
+ }
676
+ if (node.parentNode === parent) parent.removeChild(node);
581
677
  }
582
678
  }
583
679
  mappedNodes.length = 0;
@@ -602,7 +698,7 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
602
698
  } else {
603
699
  accessor = item; // raw mode: pass item directly
604
700
  }
605
- const node = createRoot(dispose => {
701
+ const node = _createItemScope(dispose => {
606
702
  disposeFns[idx] = dispose;
607
703
  return mapFn(accessor, idx);
608
704
  });
@@ -678,7 +774,7 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
678
774
  } else {
679
775
  accessor = item;
680
776
  }
681
- newMapped[i] = createRoot(dispose => {
777
+ newMapped[i] = _createItemScope(dispose => {
682
778
  newDispose[idx] = dispose;
683
779
  return mapFn(accessor, idx);
684
780
  });
@@ -747,7 +843,7 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
747
843
  } else {
748
844
  accessor = item;
749
845
  }
750
- newMapped[i] = createRoot(dispose => {
846
+ newMapped[i] = _createItemScope(dispose => {
751
847
  newDispose[idx] = dispose;
752
848
  return mapFn(accessor, idx);
753
849
  });