what-core 0.6.1 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-core",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "What Framework - The closest framework to vanilla JS",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/dom.js CHANGED
@@ -74,16 +74,20 @@ function disposeComponent(ctx) {
74
74
  mountedComponents.delete(ctx);
75
75
  }
76
76
 
77
- // Dispose all components and reactive effects attached to a DOM subtree
77
+ // Dispose all components and reactive effects attached to a DOM subtree.
78
+ // Performance: checks _componentCtx / _dispose / _propEffects before walking
79
+ // children, and only checks _commentCtxMap for comment nodes (nodeType 8).
78
80
  export function disposeTree(node) {
79
81
  if (!node) return;
80
82
  if (node._componentCtx) {
81
83
  disposeComponent(node._componentCtx);
82
84
  }
83
- // Check comment node WeakMap for component context
84
- const commentCtx = _commentCtxMap.get(node);
85
- if (commentCtx) {
86
- disposeComponent(commentCtx);
85
+ // Check comment node WeakMap for component context — only for comment nodes
86
+ if (node.nodeType === 8) {
87
+ const commentCtx = _commentCtxMap.get(node);
88
+ if (commentCtx) {
89
+ disposeComponent(commentCtx);
90
+ }
87
91
  }
88
92
  // Dispose reactive function child effects ({() => ...} wrappers)
89
93
  if (node._dispose) {
@@ -95,9 +99,11 @@ export function disposeTree(node) {
95
99
  try { node._propEffects[key](); } catch (e) { /* already disposed */ }
96
100
  }
97
101
  }
98
- if (node.childNodes) {
99
- for (const child of node.childNodes) {
100
- disposeTree(child);
102
+ // Recursively dispose children
103
+ const children = node.childNodes;
104
+ if (children && children.length > 0) {
105
+ for (let i = 0; i < children.length; i++) {
106
+ disposeTree(children[i]);
101
107
  }
102
108
  }
103
109
  }
@@ -216,6 +222,31 @@ export function createDOM(vnode, parent, isSvg) {
216
222
  // --- Component Rendering ---
217
223
  // Components run ONCE. Props are passed as a signal for reactive access.
218
224
 
225
+ // Shared Proxy handler for reactive props — defined once, reused by all components.
226
+ // The Proxy target must be a plain object (not a function) so that ownKeys
227
+ // invariants are satisfied. The propsSignal is stored as target._sig.
228
+ const _propsProxyHandler = {
229
+ get(target, key) {
230
+ if (key === '_sig') return undefined; // hide internal property
231
+ return target._sig()[key];
232
+ },
233
+ has(target, key) {
234
+ if (key === '_sig') return false;
235
+ return key in target._sig();
236
+ },
237
+ ownKeys(target) {
238
+ return Reflect.ownKeys(target._sig());
239
+ },
240
+ getOwnPropertyDescriptor(target, key) {
241
+ if (key === '_sig') return undefined;
242
+ const current = target._sig();
243
+ if (key in current) {
244
+ return { value: current[key], writable: false, enumerable: true, configurable: true };
245
+ }
246
+ return undefined;
247
+ },
248
+ };
249
+
219
250
  const componentStack = [];
220
251
 
221
252
  export function getCurrentComponent() {
@@ -256,6 +287,20 @@ function createComponent(vnode, parent, isSvg) {
256
287
  }
257
288
 
258
289
  // Component context for hooks
290
+ // Error boundary lookup: walk the parent chain once, cache the result.
291
+ const parentCtx = componentStack[componentStack.length - 1] || null;
292
+ let errorBoundary = null;
293
+ if (parentCtx) {
294
+ // Fast path: if parent has an error boundary, use it directly
295
+ errorBoundary = parentCtx._errorBoundary || null;
296
+ if (!errorBoundary) {
297
+ let p = parentCtx._parentCtx;
298
+ while (p) {
299
+ if (p._errorBoundary) { errorBoundary = p._errorBoundary; break; }
300
+ p = p._parentCtx;
301
+ }
302
+ }
303
+ }
259
304
  const ctx = {
260
305
  hooks: [],
261
306
  hookIndex: 0,
@@ -264,15 +309,8 @@ function createComponent(vnode, parent, isSvg) {
264
309
  mounted: false,
265
310
  disposed: false,
266
311
  Component,
267
- _parentCtx: componentStack[componentStack.length - 1] || null,
268
- _errorBoundary: (() => {
269
- let p = componentStack[componentStack.length - 1];
270
- while (p) {
271
- if (p._errorBoundary) return p._errorBoundary;
272
- p = p._parentCtx;
273
- }
274
- return null;
275
- })()
312
+ _parentCtx: parentCtx,
313
+ _errorBoundary: errorBoundary,
276
314
  };
277
315
 
278
316
  // Component boundaries: use comment nodes instead of <span style="display:contents">
@@ -294,35 +332,24 @@ function createComponent(vnode, parent, isSvg) {
294
332
 
295
333
  // Props signal for reactive updates from parent
296
334
  const propsChildren = children.length === 0 ? undefined : children.length === 1 ? children[0] : children;
297
- const propsSignal = signal({ ...props, children: propsChildren });
335
+ // Merge children into props without spreading when possible
336
+ let mergedProps;
337
+ if (propsChildren !== undefined) {
338
+ mergedProps = props ? Object.assign({}, props, { children: propsChildren }) : { children: propsChildren };
339
+ } else {
340
+ mergedProps = props ? Object.assign({}, props) : {};
341
+ }
342
+ const propsSignal = signal(mergedProps);
298
343
  ctx._propsSignal = propsSignal;
299
344
 
300
345
  // Create a reactive props proxy: reading any prop inside an effect
301
346
  // will auto-track the dependency on the propsSignal. This makes prop
302
347
  // access reactive across re-renders without requiring the component
303
348
  // to be re-executed.
304
- const reactiveProps = new Proxy({}, {
305
- get(_, key) {
306
- // Access the signal to create a reactive dependency
307
- const current = propsSignal();
308
- return current[key];
309
- },
310
- has(_, key) {
311
- const current = propsSignal();
312
- return key in current;
313
- },
314
- ownKeys() {
315
- const current = propsSignal();
316
- return Reflect.ownKeys(current);
317
- },
318
- getOwnPropertyDescriptor(_, key) {
319
- const current = propsSignal();
320
- if (key in current) {
321
- return { value: current[key], writable: false, enumerable: true, configurable: true };
322
- }
323
- return undefined;
324
- },
325
- });
349
+ // Reuse shared trap handlers to minimize per-component allocation.
350
+ // Store propsSignal on a plain object target (Proxy invariant: ownKeys must
351
+ // match non-configurable own properties of target; functions have 'prototype').
352
+ const reactiveProps = new Proxy({ _sig: propsSignal }, _propsProxyHandler);
326
353
 
327
354
  // Component runs ONCE — not inside an effect
328
355
  componentStack.push(ctx);
@@ -550,8 +577,9 @@ function createElementFromVNode(vnode, parent, isSvg) {
550
577
  }
551
578
 
552
579
  // Append children
553
- for (const child of children) {
554
- const node = createDOM(child, el, svgContext && tag !== 'foreignObject');
580
+ const isSvgChildren = svgContext && tag !== 'foreignObject';
581
+ for (let i = 0; i < children.length; i++) {
582
+ const node = createDOM(children[i], el, isSvgChildren);
555
583
  if (node) el.appendChild(node);
556
584
  }
557
585
 
@@ -563,16 +591,16 @@ function createElementFromVNode(vnode, parent, isSvg) {
563
591
  // Only applied once for fine-grained (no diffing). Reactive props use effects.
564
592
 
565
593
  function applyProps(el, newProps, oldProps, isSvg) {
566
- newProps = newProps || {};
567
- oldProps = oldProps || {};
594
+ if (!newProps) return;
568
595
 
569
596
  for (const key in newProps) {
570
597
  if (key === 'key' || key === 'children') continue;
571
598
 
572
599
  // Handle ref
573
600
  if (key === 'ref') {
574
- if (typeof newProps.ref === 'function') newProps.ref(el);
575
- else if (newProps.ref) newProps.ref.current = el;
601
+ const ref = newProps.ref;
602
+ if (typeof ref === 'function') ref(el);
603
+ else if (ref) ref.current = el;
576
604
  continue;
577
605
  }
578
606
 
@@ -609,10 +637,13 @@ function setProp(el, key, value, isSvg) {
609
637
  if (old) el.removeEventListener(event, old, useCapture);
610
638
  if (value == null) return;
611
639
  if (!el._events) el._events = {};
640
+ // Single closure per event listener. Uses untrack to prevent accidental
641
+ // signal subscriptions inside event handlers.
612
642
  const wrappedHandler = (e) => {
613
643
  if (!e.nativeEvent) e.nativeEvent = e;
614
- return untrack(() => value(e));
644
+ return untrack(() => wrappedHandler._handler(e));
615
645
  };
646
+ wrappedHandler._handler = value;
616
647
  wrappedHandler._original = value;
617
648
  el._events[storageKey] = wrappedHandler;
618
649
  const eventOpts = value._eventOpts;
package/src/h.js CHANGED
@@ -8,9 +8,25 @@
8
8
  const EMPTY_OBJ = Object.create(null);
9
9
  const EMPTY_ARR = [];
10
10
 
11
- export function h(tag, props, ...children) {
11
+ export function h(tag, props) {
12
12
  props = props || EMPTY_OBJ;
13
- const flat = flattenChildren(children);
13
+ // Collect children from arguments[2..n] without rest args — avoids array allocation
14
+ // when there are 0-1 children (common case: leaf elements, text nodes).
15
+ const argLen = arguments.length;
16
+ let flat;
17
+ if (argLen <= 2) {
18
+ flat = EMPTY_ARR;
19
+ } else if (argLen === 3) {
20
+ flat = _flattenSingle(arguments[2]);
21
+ } else {
22
+ // Multiple children: flatten all arguments from position 2 onward
23
+ const out = [];
24
+ for (let i = 2; i < argLen; i++) {
25
+ _flattenInto(arguments[i], out);
26
+ }
27
+ flat = out;
28
+ }
29
+
14
30
  const key = props.key ?? null;
15
31
 
16
32
  // Strip key from props passed to component/element
@@ -27,24 +43,33 @@ export function Fragment({ children }) {
27
43
  return children;
28
44
  }
29
45
 
30
- function flattenChildren(children) {
31
- const out = [];
32
- for (let i = 0; i < children.length; i++) {
33
- const child = children[i];
34
- if (child == null || child === false || child === true) continue;
35
- if (Array.isArray(child)) {
36
- out.push(...flattenChildren(child));
37
- } else if (typeof child === 'object' && child._vnode) {
38
- out.push(child);
39
- } else if (typeof child === 'function') {
40
- // Reactive child — preserve function for fine-grained DOM updates
41
- out.push(child);
42
- } else {
43
- // Text node
44
- out.push(String(child));
46
+ // Fast path for single child (most common case)
47
+ function _flattenSingle(child) {
48
+ if (child == null || child === false || child === true) return EMPTY_ARR;
49
+ if (Array.isArray(child)) {
50
+ const out = [];
51
+ _flattenInto(child, out);
52
+ return out;
53
+ }
54
+ if (typeof child === 'object' && child._vnode) return [child];
55
+ if (typeof child === 'function') return [child];
56
+ return [String(child)];
57
+ }
58
+
59
+ // Flatten a child (or array of children) into the output array
60
+ function _flattenInto(child, out) {
61
+ if (child == null || child === false || child === true) return;
62
+ if (Array.isArray(child)) {
63
+ for (let i = 0; i < child.length; i++) {
64
+ _flattenInto(child[i], out);
45
65
  }
66
+ } else if (typeof child === 'object' && child._vnode) {
67
+ out.push(child);
68
+ } else if (typeof child === 'function') {
69
+ out.push(child);
70
+ } else {
71
+ out.push(String(child));
46
72
  }
47
- return out;
48
73
  }
49
74
 
50
75
  // JSX-like tagged template alternative (no build step needed)