@ape-egg/vibe 1.7.2 → 1.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/CHANGELOG.md +51 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/Cargo.lock +85 -496
- package/compiler/src/Cargo.toml +1 -1
- package/compiler/src/compiler/compile.rs +3 -11
- package/package.json +1 -1
- package/runtime/affected.js +18 -0
- package/runtime/component.js +147 -79
- package/runtime/conditionals.js +88 -7
- package/runtime/constants.js +7 -5
- package/runtime/index.js +65 -14
- package/runtime/iterate.js +119 -59
- package/runtime/iteration-utils.js +5 -1
- package/runtime/parse.js +1 -0
- package/runtime/pre-compiled-iterations.js +34 -21
- package/runtime/state.js +21 -5
- package/runtime/utils.js +24 -5
package/runtime/iterate.js
CHANGED
|
@@ -3,6 +3,7 @@ import affected from './affected.js';
|
|
|
3
3
|
import hydrate from './hydrate.js';
|
|
4
4
|
import { resolvePath, getItemKey, computeDiff } from './iteration-utils.js';
|
|
5
5
|
import { resolveThisPath } from './utils.js';
|
|
6
|
+
import { managedNodes } from './conditionals.js';
|
|
6
7
|
|
|
7
8
|
// Fast path for iteration rendering (opt-in via window.__VIBE_FAST_ITERATION__)
|
|
8
9
|
// See: _vibe-compiled-iteration-batch.js for implementation details
|
|
@@ -138,32 +139,31 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null) =
|
|
|
138
139
|
let clonedNodes = [];
|
|
139
140
|
let firstElement = null;
|
|
140
141
|
|
|
141
|
-
//
|
|
142
|
-
|
|
142
|
+
// Use cached tree when available: cloneTreeWithElements maps the existing parsed structure
|
|
143
|
+
// onto cloned DOM nodes, avoiding a full parse() call per iteration item.
|
|
144
|
+
// Only fall back to parse() when no cached tree exists (first parse of a new template).
|
|
145
|
+
// cloneTreeWithElements has a mapping bug with compiled mode's tree structure.
|
|
146
|
+
// Keep disabled until the root cause is fixed — the other optimizations
|
|
147
|
+
// (bulk replacement, evalInScope caching, DocumentFragment) cover the hot paths.
|
|
148
|
+
const useCachedTree = false;
|
|
143
149
|
|
|
144
150
|
// Create a container for parsing (to capture all nodes including comment nodes like <!-- if -->)
|
|
145
151
|
const parseContainer = document.createElement('div');
|
|
146
152
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
firstElement = cloned;
|
|
154
|
-
}
|
|
153
|
+
// Clone template nodes into container
|
|
154
|
+
for (let i = 0; i < templateNodes.length; i++) {
|
|
155
|
+
const cloned = templateNodes[i].cloneNode(true);
|
|
156
|
+
parseContainer.appendChild(cloned);
|
|
157
|
+
if (!firstElement && cloned.nodeType === 1) {
|
|
158
|
+
firstElement = cloned;
|
|
155
159
|
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (useCachedTree) {
|
|
163
|
+
// Fast path: map cached tree structure onto cloned DOM (no regex, no DOM walking)
|
|
156
164
|
tree = cloneTreeWithElements(cachedTree, parseContainer);
|
|
157
165
|
} else {
|
|
158
|
-
//
|
|
159
|
-
for (let i = 0; i < templateNodes.length; i++) {
|
|
160
|
-
const cloned = templateNodes[i].cloneNode(true);
|
|
161
|
-
parseContainer.appendChild(cloned);
|
|
162
|
-
if (!firstElement && cloned.nodeType === 1) {
|
|
163
|
-
firstElement = cloned;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
// Parse the entire container (includes all nodes + conditionals)
|
|
166
|
+
// Full parse: walk DOM, extract bindings, build tree from scratch
|
|
167
167
|
tree = parse(parseContainer);
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -194,24 +194,22 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null) =
|
|
|
194
194
|
|
|
195
195
|
// Create a proxied state with scoped variables (item, index, array)
|
|
196
196
|
export const createScopedState = (globalState, localVars, parentScope = {}) => {
|
|
197
|
+
// Pre-compute the combined key list once at creation time.
|
|
198
|
+
// Avoids rebuilding 3 arrays + Set on every Object.keys() call.
|
|
199
|
+
const cachedKeys = [...new Set([
|
|
200
|
+
...Object.keys(localVars),
|
|
201
|
+
...Object.keys(parentScope),
|
|
202
|
+
...Reflect.ownKeys(globalState),
|
|
203
|
+
])];
|
|
204
|
+
|
|
197
205
|
return new Proxy(globalState, {
|
|
198
206
|
get(target, prop) {
|
|
199
|
-
|
|
200
|
-
if (prop in
|
|
201
|
-
return localVars[prop];
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// 2. Check parent scope (for nested iterations)
|
|
205
|
-
if (prop in parentScope) {
|
|
206
|
-
return parentScope[prop];
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// 3. Fall back to global state
|
|
207
|
+
if (prop in localVars) return localVars[prop];
|
|
208
|
+
if (prop in parentScope) return parentScope[prop];
|
|
210
209
|
return Reflect.get(target, prop);
|
|
211
210
|
},
|
|
212
211
|
|
|
213
212
|
set(target, prop, value) {
|
|
214
|
-
// Only allow setting global state, not local vars
|
|
215
213
|
if (prop in localVars) {
|
|
216
214
|
console.warn(`Cannot modify iteration variable '${prop}'`);
|
|
217
215
|
return false;
|
|
@@ -223,21 +221,13 @@ export const createScopedState = (globalState, localVars, parentScope = {}) => {
|
|
|
223
221
|
return Reflect.set(target, prop, value);
|
|
224
222
|
},
|
|
225
223
|
|
|
226
|
-
ownKeys(
|
|
227
|
-
// Return all keys: local vars, parent scope, and global state
|
|
228
|
-
const localKeys = Object.keys(localVars);
|
|
229
|
-
const parentKeys = Object.keys(parentScope);
|
|
230
|
-
const globalKeys = Reflect.ownKeys(target);
|
|
231
|
-
return [...new Set([...localKeys, ...parentKeys, ...globalKeys])];
|
|
232
|
-
},
|
|
224
|
+
ownKeys() { return cachedKeys; },
|
|
233
225
|
|
|
234
226
|
has(target, prop) {
|
|
235
|
-
// Check if property exists in local scope, parent scope, or global state
|
|
236
227
|
return prop in localVars || prop in parentScope || Reflect.has(target, prop);
|
|
237
228
|
},
|
|
238
229
|
|
|
239
230
|
getOwnPropertyDescriptor(target, prop) {
|
|
240
|
-
// Provide property descriptor for local vars and parent scope
|
|
241
231
|
if (prop in localVars) {
|
|
242
232
|
return { configurable: true, enumerable: true, value: localVars[prop] };
|
|
243
233
|
}
|
|
@@ -344,15 +334,10 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
344
334
|
// Fall through to runtime path if compiled failed
|
|
345
335
|
}
|
|
346
336
|
|
|
347
|
-
// Fast path: opt-in via window.__VIBE_FAST_ITERATION__ (preview of Vibe Compiled)
|
|
348
|
-
if (window.__VIBE_FAST_ITERATION__ && fastPath.canUseFastPath(template)) {
|
|
349
|
-
fastPath.renderFast(iterationNode, array, state, parent, endComment);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
337
|
// Standard path: clone and hydrate each item (handles nested iterations/conditionals)
|
|
354
338
|
const instances = [];
|
|
355
|
-
const templateNodes =
|
|
339
|
+
const templateNodes = template.element.childNodes;
|
|
340
|
+
const frag = document.createDocumentFragment();
|
|
356
341
|
|
|
357
342
|
for (let i = 0; i < array.length; i++) {
|
|
358
343
|
const item = array[i];
|
|
@@ -362,8 +347,11 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
362
347
|
// Clone, parse, hydrate
|
|
363
348
|
const { element, tree, clonedNodes } = initializeBlock(templateNodes, scopedState, template);
|
|
364
349
|
|
|
365
|
-
//
|
|
366
|
-
|
|
350
|
+
// Collect nodes in DocumentFragment (single DOM insertion at end)
|
|
351
|
+
for (let j = 0; j < clonedNodes.length; j++) {
|
|
352
|
+
frag.appendChild(clonedNodes[j]);
|
|
353
|
+
if (clonedNodes[j].nodeType === 1) managedNodes.add(clonedNodes[j]);
|
|
354
|
+
}
|
|
367
355
|
|
|
368
356
|
// Recursively render nested iterations and conditionals
|
|
369
357
|
if (tree) {
|
|
@@ -375,6 +363,8 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
375
363
|
instances.push({ element, tree, item, index: i, clonedNodes, scopedState });
|
|
376
364
|
}
|
|
377
365
|
|
|
366
|
+
// Single DOM insertion for all items
|
|
367
|
+
parent.insertBefore(frag, endComment);
|
|
378
368
|
iterationNode.runtime.instances = instances;
|
|
379
369
|
|
|
380
370
|
// Mark comment as rendered (survives re-parsing since comment nodes persist in DOM)
|
|
@@ -394,9 +384,16 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
394
384
|
// Handle this.property for component-scoped arrays
|
|
395
385
|
const resolvedArrayPath = resolveThisPath(arrayPath, startComment.parentElement);
|
|
396
386
|
|
|
397
|
-
const
|
|
387
|
+
const stateOldArray = resolvePath(oldState, resolvedArrayPath) || [];
|
|
398
388
|
const newArray = resolvePath(newState, resolvedArrayPath) || [];
|
|
399
389
|
|
|
390
|
+
// Use instances (what's actually rendered) as ground truth for old array
|
|
391
|
+
// when oldState disagrees with the rendered count.
|
|
392
|
+
const instances = iterationNode.runtime.instances;
|
|
393
|
+
const oldArray = instances.length === stateOldArray.length
|
|
394
|
+
? stateOldArray
|
|
395
|
+
: instances.map(inst => inst.item);
|
|
396
|
+
|
|
400
397
|
// Compiled path: Use pre-compiled batch function when available
|
|
401
398
|
if (compiled.canUseCompiled(iterationNode)) {
|
|
402
399
|
const compiledMeta = compiled.getCompiledMeta(iterationNode);
|
|
@@ -415,20 +412,24 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
415
412
|
// Fall through to runtime path if compiled failed
|
|
416
413
|
}
|
|
417
414
|
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
fastPath.canUseFastPath(template) &&
|
|
423
|
-
(isEmptyToFull || isFullToEmpty || isLargeArray)
|
|
424
|
-
) {
|
|
425
|
-
fastPath.updateFast(iterationNode, newArray, newState, startComment, endComment);
|
|
415
|
+
// Bulk path: skip O(n²) LCS when arrays share no common items
|
|
416
|
+
// Handles empty→full, full→empty, and full replacement (no shared keys)
|
|
417
|
+
if (oldArray.length === 0 || newArray.length === 0) {
|
|
418
|
+
bulkReplace(iterationNode, newArray, newState, manifest, parentScope);
|
|
426
419
|
return;
|
|
427
420
|
}
|
|
428
421
|
|
|
429
|
-
// Standard diff-based updates
|
|
430
422
|
const oldKeys = oldArray.map((item, i) => getItemKey(item, i));
|
|
431
423
|
const newKeys = newArray.map((item, i) => getItemKey(item, i));
|
|
424
|
+
|
|
425
|
+
// O(n) check: if no keys are shared, do bulk replacement instead of O(n²) LCS
|
|
426
|
+
const oldKeySet = new Set(oldKeys);
|
|
427
|
+
if (!newKeys.some(k => oldKeySet.has(k))) {
|
|
428
|
+
bulkReplace(iterationNode, newArray, newState, manifest, parentScope);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Standard diff-based updates (arrays share some common items)
|
|
432
433
|
const operations = computeDiff(oldKeys, newKeys, oldArray, newArray);
|
|
433
434
|
|
|
434
435
|
operations.forEach((op) => {
|
|
@@ -453,6 +454,65 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
453
454
|
});
|
|
454
455
|
};
|
|
455
456
|
|
|
457
|
+
// Bulk replacement: clear all DOM and re-render from scratch
|
|
458
|
+
// Used when arrays share no common keys (avoids O(n²) LCS)
|
|
459
|
+
const bulkReplace = (iterationNode, newArray, state, manifest, parentScope) => {
|
|
460
|
+
const { itemAlias, indexAlias, template, startComment, endComment } = iterationNode.meta;
|
|
461
|
+
const parent = startComment.parentNode;
|
|
462
|
+
|
|
463
|
+
// Clear all existing DOM between comments using Range (single operation)
|
|
464
|
+
if (iterationNode.runtime.instances.length > 0) {
|
|
465
|
+
const range = document.createRange();
|
|
466
|
+
range.setStartAfter(startComment);
|
|
467
|
+
range.setEndBefore(endComment);
|
|
468
|
+
range.deleteContents();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (newArray.length === 0) {
|
|
472
|
+
iterationNode.runtime.instances = [];
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// For simple templates (no nested iterations/conditionals, single root element),
|
|
477
|
+
// use batch string rendering: one string concatenation loop + one innerHTML parse
|
|
478
|
+
if (fastPath.canUseFastPath(template) && template.element.children.length <= 1) {
|
|
479
|
+
fastPath.renderFast(iterationNode, newArray, state, parent, endComment);
|
|
480
|
+
const instances = iterationNode.runtime.instances;
|
|
481
|
+
for (let i = 0; i < instances.length; i++) {
|
|
482
|
+
if (instances[i].element?.nodeType === 1) managedNodes.add(instances[i].element);
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Complex templates: clone, parse, hydrate each item with DocumentFragment batching
|
|
488
|
+
const instances = [];
|
|
489
|
+
const templateNodes = template.element.childNodes;
|
|
490
|
+
const frag = document.createDocumentFragment();
|
|
491
|
+
|
|
492
|
+
for (let i = 0; i < newArray.length; i++) {
|
|
493
|
+
const item = newArray[i];
|
|
494
|
+
const localVars = { [itemAlias]: item, [indexAlias]: i };
|
|
495
|
+
const scopedState = createScopedState(state, localVars, parentScope);
|
|
496
|
+
const { element, tree, clonedNodes } = initializeBlock(templateNodes, scopedState, template);
|
|
497
|
+
|
|
498
|
+
for (let j = 0; j < clonedNodes.length; j++) {
|
|
499
|
+
frag.appendChild(clonedNodes[j]);
|
|
500
|
+
if (clonedNodes[j].nodeType === 1) managedNodes.add(clonedNodes[j]);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (tree) {
|
|
504
|
+
const nestedScope = { ...parentScope, ...localVars };
|
|
505
|
+
renderAllIterations(tree, scopedState, manifest, nestedScope);
|
|
506
|
+
_renderAllConditionals(tree, scopedState, manifest, nestedScope);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
instances.push({ element, tree, item, index: i, clonedNodes, scopedState });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
parent.insertBefore(frag, endComment);
|
|
513
|
+
iterationNode.runtime.instances = instances;
|
|
514
|
+
};
|
|
515
|
+
|
|
456
516
|
// Add a new instance at the specified index
|
|
457
517
|
const addInstance = (iterationNode, item, index, state, manifest, parentScope) => {
|
|
458
518
|
const { itemAlias, indexAlias, template, startComment, endComment } = iterationNode.meta;
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
import { ITERATION_START_REGEX, CONDITIONAL_START_REGEX } from './constants.js';
|
|
3
3
|
|
|
4
4
|
// Resolve nested paths in state (e.g., "user.items" -> state.user.items)
|
|
5
|
+
// Supports bracket notation: "teams[0].combatants" -> state.teams[0].combatants
|
|
5
6
|
export const resolvePath = (obj, path) => {
|
|
6
7
|
if (!path || !obj) return undefined;
|
|
7
|
-
|
|
8
|
+
// Split on dots and brackets: "a[0].b[1].c" → ["a", "0", "b", "1", "c"]
|
|
9
|
+
const parts = path.match(/[^.\[\]]+/g);
|
|
10
|
+
if (!parts) return undefined;
|
|
11
|
+
return parts.reduce((acc, part) => acc?.[part], obj);
|
|
8
12
|
};
|
|
9
13
|
|
|
10
14
|
// Clone template element preserving structure
|
package/runtime/parse.js
CHANGED
|
@@ -140,6 +140,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
140
140
|
// Store conditional metadata (use index for deterministic keys)
|
|
141
141
|
const conditionalKey = `conditional_${i}`;
|
|
142
142
|
result[conditionalKey] = {
|
|
143
|
+
_key: conditionalKey,
|
|
143
144
|
type: 'conditional',
|
|
144
145
|
meta: {
|
|
145
146
|
expression,
|
|
@@ -60,23 +60,46 @@ export const getCompiledMeta = (iterationNode) => {
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
*
|
|
63
|
+
* Build a scoped wrapper that puts state keys in scope for the batch function.
|
|
64
|
+
* The compiler generates (arr, $) => { ... } with bare variable names like `selectedCategory`,
|
|
65
|
+
* but those aren't parameters of the arrow function. We create a wrapper that defines
|
|
66
|
+
* state keys as parameters, then evaluates the batch function in that scope.
|
|
64
67
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const buildScopedFn = (batchFnStr, stateKeys) => {
|
|
69
|
+
return new Function(
|
|
70
|
+
...stateKeys, 'arr',
|
|
71
|
+
`const $ = {${stateKeys.map(k => k + ':' + k).join(',')}};
|
|
72
|
+
const __batchFn = ${batchFnStr};
|
|
73
|
+
return __batchFn(arr, $);`
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Call a compiled batch function with state keys spread into scope
|
|
79
|
+
*/
|
|
80
|
+
const callCompiled = (iterationNode, array, state, compiledMeta) => {
|
|
67
81
|
if (!iterationNode.runtime.compiledFn) {
|
|
68
82
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
iterationNode.runtime.
|
|
83
|
+
const stateKeys = Object.keys(state);
|
|
84
|
+
iterationNode.runtime.compiledFn = buildScopedFn(compiledMeta.batchFn, stateKeys);
|
|
85
|
+
iterationNode.runtime.compiledStateKeys = stateKeys;
|
|
72
86
|
} catch (e) {
|
|
73
87
|
console.error('[compiled-iteration] Failed to create compiled function:', e);
|
|
74
|
-
return
|
|
88
|
+
return null;
|
|
75
89
|
}
|
|
76
90
|
}
|
|
77
91
|
|
|
78
|
-
|
|
79
|
-
const
|
|
92
|
+
const keys = iterationNode.runtime.compiledStateKeys;
|
|
93
|
+
const values = keys.map(k => state[k]);
|
|
94
|
+
return iterationNode.runtime.compiledFn(...values, array);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Render iteration using pre-compiled batch function from manifest
|
|
99
|
+
*/
|
|
100
|
+
export const renderCompiled = (iterationNode, array, state, compiledMeta, parent, endComment) => {
|
|
101
|
+
const html = callCompiled(iterationNode, array, state, compiledMeta);
|
|
102
|
+
if (html === null) return false;
|
|
80
103
|
|
|
81
104
|
// Parse and insert
|
|
82
105
|
if (parseTemplate) {
|
|
@@ -118,18 +141,8 @@ export const updateCompiled = (iterationNode, newArray, state, compiledMeta, sta
|
|
|
118
141
|
return true;
|
|
119
142
|
}
|
|
120
143
|
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
try {
|
|
124
|
-
iterationNode.runtime.compiledFn = new Function('return ' + compiledMeta.batchFn)();
|
|
125
|
-
} catch (e) {
|
|
126
|
-
console.error('Failed to create compiled function:', e);
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Build HTML using compiled function
|
|
132
|
-
const html = iterationNode.runtime.compiledFn(newArray, state);
|
|
144
|
+
const html = callCompiled(iterationNode, newArray, state, compiledMeta);
|
|
145
|
+
if (html === null) return false;
|
|
133
146
|
|
|
134
147
|
// Parse and insert
|
|
135
148
|
if (parseTemplate) {
|
package/runtime/state.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
// Track which objects are already proxied to avoid double-wrapping
|
|
2
2
|
const proxyCache = new WeakMap();
|
|
3
3
|
|
|
4
|
+
// Batching: collect mutations and flush once per microtask
|
|
5
|
+
let pendingFlush = false;
|
|
6
|
+
let flushCallback = null;
|
|
7
|
+
const changedProps = new Set();
|
|
8
|
+
|
|
9
|
+
const scheduleFlush = () => {
|
|
10
|
+
if (!pendingFlush) {
|
|
11
|
+
pendingFlush = true;
|
|
12
|
+
queueMicrotask(() => {
|
|
13
|
+
pendingFlush = false;
|
|
14
|
+
const props = [...changedProps];
|
|
15
|
+
changedProps.clear();
|
|
16
|
+
if (flushCallback) flushCallback(props);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
4
21
|
// Deep proxy: recursively wrap nested objects and arrays
|
|
5
22
|
const createDeepProxy = (target, rerender, rootState = null, rootProp = null) => {
|
|
6
23
|
// For root level, rootState is the target itself
|
|
7
24
|
if (rootState === null) {
|
|
8
25
|
rootState = target;
|
|
26
|
+
flushCallback = (props) => rerender(props);
|
|
9
27
|
}
|
|
10
28
|
|
|
11
29
|
// Check cache first
|
|
@@ -19,13 +37,11 @@ const createDeepProxy = (target, rerender, rootState = null, rootProp = null) =>
|
|
|
19
37
|
|
|
20
38
|
// Only trigger rerender if value actually changed
|
|
21
39
|
if (oldValue !== value) {
|
|
22
|
-
// Perform the mutation
|
|
23
40
|
const ref = Reflect.set(obj, prop, value);
|
|
24
41
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
rerender({ [changedProp]: rootState[changedProp] }, null);
|
|
42
|
+
// Track which root-level property changed (for selective extraction)
|
|
43
|
+
changedProps.add(rootProp || prop);
|
|
44
|
+
scheduleFlush();
|
|
29
45
|
|
|
30
46
|
return ref;
|
|
31
47
|
}
|
package/runtime/utils.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
let hashCounter = 0;
|
|
3
3
|
export const hash = () => `_${hashCounter++}`;
|
|
4
4
|
|
|
5
|
+
// Function compilation cache: avoids creating new Function() for repeated expressions
|
|
6
|
+
// Key: normalized expression + '\0' + state keys joined by '\0'
|
|
7
|
+
const fnCache = new Map();
|
|
8
|
+
|
|
5
9
|
// Evaluate expression in the context of state
|
|
6
10
|
// Handles @[this.property] for component state and @[property] for global state
|
|
7
11
|
export const evalInScope = (expr, state, element = null) => {
|
|
@@ -18,13 +22,28 @@ export const evalInScope = (expr, state, element = null) => {
|
|
|
18
22
|
}
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
//
|
|
22
|
-
const
|
|
25
|
+
// Get state keys once (triggers ownKeys trap only once for Proxies)
|
|
26
|
+
const stateKeys = Object.keys(state);
|
|
27
|
+
const keyCount = stateKeys.length;
|
|
28
|
+
|
|
29
|
+
// Cache lookup: expression + key signature → compiled function
|
|
30
|
+
// All iteration instances share the same state shape, so this hits cache 999/1000 times
|
|
31
|
+
const cacheKey = normalized + '\0' + stateKeys.join('\0');
|
|
32
|
+
let fn = fnCache.get(cacheKey);
|
|
33
|
+
if (!fn) {
|
|
34
|
+
const allKeys = new Array(keyCount + 1);
|
|
35
|
+
for (let i = 0; i < keyCount; i++) allKeys[i] = stateKeys[i];
|
|
36
|
+
allKeys[keyCount] = '$';
|
|
37
|
+
fn = new Function(...allKeys, `'use strict'; return (${normalized})`);
|
|
38
|
+
fnCache.set(cacheKey, fn);
|
|
39
|
+
}
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
const values =
|
|
41
|
+
// Build values array matching the cached function's parameter order
|
|
42
|
+
const values = new Array(keyCount + 1);
|
|
43
|
+
for (let i = 0; i < keyCount; i++) values[i] = state[stateKeys[i]];
|
|
44
|
+
values[keyCount] = state;
|
|
26
45
|
|
|
27
|
-
const result =
|
|
46
|
+
const result = fn(...values);
|
|
28
47
|
|
|
29
48
|
// If result is undefined and we're accessing a component property, try case-insensitive match
|
|
30
49
|
// This handles HTML lowercasing attribute names like @[this.iconName] -> @[this.iconname]
|