elementdrawing 1.0.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/LICENSE +21 -0
- package/dist/elementdrawing.min.js +3 -0
- package/dist/elementdrawing.min.js.LICENSE.txt +8 -0
- package/dist/elementdrawing.min.js.map +1 -0
- package/dist/index.html +1 -0
- package/package.json +127 -0
- package/src/core/bridge.h +855 -0
- package/src/core/diff.c +900 -0
- package/src/core/element.c +1078 -0
- package/src/core/event.c +813 -0
- package/src/core/fiber.c +1027 -0
- package/src/core/hooks.c +919 -0
- package/src/core/renderer.c +963 -0
- package/src/core/scheduler.c +702 -0
- package/src/core/state.c +803 -0
- package/src/css/animations.css +779 -0
- package/src/css/base.css +615 -0
- package/src/css/components.css +1311 -0
- package/src/css/tailwind.css +370 -0
- package/src/css/themes.css +517 -0
- package/src/css/utilities.css +475 -0
- package/src/index.js +746 -0
- package/src/js/animation.js +655 -0
- package/src/js/dom.js +665 -0
- package/src/js/events.js +585 -0
- package/src/js/http.js +446 -0
- package/src/js/index.js +26 -0
- package/src/js/router.js +483 -0
- package/src/js/store.js +539 -0
- package/src/js/utils.js +593 -0
- package/src/js/validator.js +529 -0
- package/src/jsx/components/Accordion.jsx +210 -0
- package/src/jsx/components/Alert.jsx +169 -0
- package/src/jsx/components/Avatar.jsx +214 -0
- package/src/jsx/components/Badge.jsx +136 -0
- package/src/jsx/components/Breadcrumb.jsx +200 -0
- package/src/jsx/components/Button.jsx +188 -0
- package/src/jsx/components/Card.jsx +192 -0
- package/src/jsx/components/Carousel.jsx +278 -0
- package/src/jsx/components/Checkbox.jsx +215 -0
- package/src/jsx/components/Dialog.jsx +242 -0
- package/src/jsx/components/Drawer.jsx +190 -0
- package/src/jsx/components/Dropdown.jsx +268 -0
- package/src/jsx/components/Form.jsx +274 -0
- package/src/jsx/components/Input.jsx +285 -0
- package/src/jsx/components/Menu.jsx +276 -0
- package/src/jsx/components/Modal.jsx +274 -0
- package/src/jsx/components/Navbar.jsx +292 -0
- package/src/jsx/components/Pagination.jsx +268 -0
- package/src/jsx/components/Progress.jsx +252 -0
- package/src/jsx/components/Radio.jsx +208 -0
- package/src/jsx/components/Select.jsx +397 -0
- package/src/jsx/components/Sidebar.jsx +250 -0
- package/src/jsx/components/Slider.jsx +310 -0
- package/src/jsx/components/Spinner.jsx +198 -0
- package/src/jsx/components/Switch.jsx +201 -0
- package/src/jsx/components/Table.jsx +332 -0
- package/src/jsx/components/Tabs.jsx +227 -0
- package/src/jsx/components/Textarea.jsx +212 -0
- package/src/jsx/components/Toast.jsx +270 -0
- package/src/jsx/components/Tooltip.jsx +178 -0
- package/src/jsx/components/Typography.jsx +299 -0
- package/src/jsx/components/index.jsx +70 -0
- package/src/jsx/core/element.js +3 -0
- package/src/jsx/hooks/index.js +356 -0
- package/src/jsx/hooks/useCallback.js +472 -0
- package/src/jsx/hooks/useContext.js +586 -0
- package/src/jsx/hooks/useEffect.js +704 -0
- package/src/jsx/hooks/useLayoutEffect.js +508 -0
- package/src/jsx/hooks/useMemo.js +689 -0
- package/src/jsx/hooks/useReducer.js +729 -0
- package/src/jsx/hooks/useRef.js +542 -0
- package/src/jsx/hooks/useState.js +854 -0
- package/src/jsx/runtime/commit.js +903 -0
- package/src/jsx/runtime/createElement.js +860 -0
- package/src/jsx/runtime/index.js +356 -0
- package/src/jsx/runtime/reconcile.js +687 -0
- package/src/jsx/runtime/render.js +914 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reconcile - Reconciliation Algorithm
|
|
3
|
+
* ElementDrawing Framework - Diffs old and new virtual DOM trees to determine
|
|
4
|
+
* the minimal set of DOM operations needed. Includes key-based child
|
|
5
|
+
* reconciliation, fiber-based scheduling hints, prop/style/event diffing,
|
|
6
|
+
* memo optimization, and time-slicing support.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { REACT_FRAGMENT_TYPE, isValidElement } = require('./createElement');
|
|
12
|
+
|
|
13
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const INSERT = 'INSERT';
|
|
16
|
+
const MOVE = 'MOVE';
|
|
17
|
+
const REMOVE = 'REMOVE';
|
|
18
|
+
const UPDATE = 'UPDATE';
|
|
19
|
+
const REPLACE = 'REPLACE';
|
|
20
|
+
const SKIP = 'SKIP';
|
|
21
|
+
const REORDER = 'REORDER';
|
|
22
|
+
|
|
23
|
+
// ─── Reconciliation Priority ──────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const RECONCILE_PRIORITY = {
|
|
26
|
+
IMMEDIATE: 0,
|
|
27
|
+
NORMAL: 1,
|
|
28
|
+
LOW: 2,
|
|
29
|
+
IDLE: 3,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ─── Diff Result Types ────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a diff operation.
|
|
36
|
+
*/
|
|
37
|
+
function createDiffOp(type, payload) {
|
|
38
|
+
return { type, payload, _isDiffOp: true };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Fiber Node Creation ──────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a lightweight fiber node for reconciliation tracking.
|
|
45
|
+
* @param {Object|null} vnode - Virtual DOM element
|
|
46
|
+
* @param {Object|null} parentFiber - Parent fiber
|
|
47
|
+
* @returns {Object} Fiber node
|
|
48
|
+
*/
|
|
49
|
+
function createFiber(vnode, parentFiber) {
|
|
50
|
+
return {
|
|
51
|
+
vnode,
|
|
52
|
+
parent: parentFiber,
|
|
53
|
+
child: null,
|
|
54
|
+
sibling: null,
|
|
55
|
+
alternate: null,
|
|
56
|
+
effectTag: null,
|
|
57
|
+
deps: null,
|
|
58
|
+
index: 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Tree Diffing ─────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reconcile an old virtual tree with a new one, producing a list of changes.
|
|
66
|
+
*
|
|
67
|
+
* @param {Object|null} oldVNode - Previous virtual DOM tree
|
|
68
|
+
* @param {Object|null} newVNode - New virtual DOM tree
|
|
69
|
+
* @param {Object} [options] - Reconciliation options
|
|
70
|
+
* @param {number} [options.priority] - Reconciliation priority
|
|
71
|
+
* @param {number} [options.timeoutMs] - Time-slicing timeout
|
|
72
|
+
* @returns {Object} Reconciliation result with operations list
|
|
73
|
+
*/
|
|
74
|
+
function reconcile(oldVNode, newVNode, options = {}) {
|
|
75
|
+
const operations = [];
|
|
76
|
+
const context = {
|
|
77
|
+
operations,
|
|
78
|
+
parentDOM: null,
|
|
79
|
+
oldParentVNode: oldVNode,
|
|
80
|
+
newParentVNode: newVNode,
|
|
81
|
+
priority: options.priority || RECONCILE_PRIORITY.NORMAL,
|
|
82
|
+
startTime: Date.now(),
|
|
83
|
+
timeoutMs: options.timeoutMs || Infinity,
|
|
84
|
+
aborted: false,
|
|
85
|
+
stats: {
|
|
86
|
+
nodesCompared: 0,
|
|
87
|
+
operationsGenerated: 0,
|
|
88
|
+
timeElapsed: 0,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
reconcileElement(oldVNode, newVNode, operations, context);
|
|
93
|
+
|
|
94
|
+
// Update stats
|
|
95
|
+
context.stats.timeElapsed = Date.now() - context.startTime;
|
|
96
|
+
context.stats.operationsGenerated = operations.length;
|
|
97
|
+
|
|
98
|
+
return { operations, context };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if time-slicing should yield.
|
|
103
|
+
* @param {Object} context
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
function shouldYield(context) {
|
|
107
|
+
if (context.aborted) return true;
|
|
108
|
+
if (context.timeoutMs === Infinity) return false;
|
|
109
|
+
return Date.now() - context.startTime > context.timeoutMs;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Reconcile children of two vnodes.
|
|
114
|
+
* @param {Object|null} oldVNode
|
|
115
|
+
* @param {Object|null} newVNode
|
|
116
|
+
* @param {Array} operations
|
|
117
|
+
* @param {Object} context
|
|
118
|
+
*/
|
|
119
|
+
function reconcileChildren(oldVNode, newVNode, operations, context) {
|
|
120
|
+
const oldChildren = normalizeChildren(oldVNode);
|
|
121
|
+
const newChildren = normalizeChildren(newVNode);
|
|
122
|
+
|
|
123
|
+
// Key-based reconciliation for efficient child reordering
|
|
124
|
+
const oldKeyMap = buildKeyMap(oldChildren);
|
|
125
|
+
const newKeyMap = buildKeyMap(newChildren);
|
|
126
|
+
|
|
127
|
+
// ─── Phase 1: Match keyed children using Longest Increasing Subsequence ──
|
|
128
|
+
|
|
129
|
+
const matchedPairs = [];
|
|
130
|
+
const removeIndices = new Set();
|
|
131
|
+
const insertNodes = [];
|
|
132
|
+
|
|
133
|
+
// Build matching table
|
|
134
|
+
let oldIndex = 0;
|
|
135
|
+
let newIndex = 0;
|
|
136
|
+
|
|
137
|
+
while (oldIndex < oldChildren.length || newIndex < newChildren.length) {
|
|
138
|
+
if (shouldYield(context)) {
|
|
139
|
+
context.aborted = true;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const oldChild = oldChildren[oldIndex];
|
|
144
|
+
const newChild = newChildren[newIndex];
|
|
145
|
+
|
|
146
|
+
if (oldIndex >= oldChildren.length) {
|
|
147
|
+
// No more old children - insert remaining new children
|
|
148
|
+
insertNodes.push({ child: newChild, beforeIndex: newIndex });
|
|
149
|
+
matchedPairs.push({ old: null, new: newChild, oldIndex: -1, newIndex });
|
|
150
|
+
newIndex++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (newIndex >= newChildren.length) {
|
|
155
|
+
// No more new children - remove remaining old children
|
|
156
|
+
removeIndices.add(oldIndex);
|
|
157
|
+
matchedPairs.push({ old: oldChild, new: null, oldIndex, newIndex: -1 });
|
|
158
|
+
oldIndex++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (oldChild.key !== null && newChild.key !== null) {
|
|
163
|
+
if (oldChild.key === newChild.key) {
|
|
164
|
+
// Same key - reconcile in place
|
|
165
|
+
matchedPairs.push({ old: oldChild, new: newChild, oldIndex, newIndex });
|
|
166
|
+
oldIndex++;
|
|
167
|
+
newIndex++;
|
|
168
|
+
} else {
|
|
169
|
+
// Keys don't match
|
|
170
|
+
if (newKeyMap.has(oldChild.key) && newKeyMap.get(oldChild.key).index >= newIndex) {
|
|
171
|
+
insertNodes.push({ child: newChild, beforeIndex: newIndex });
|
|
172
|
+
newIndex++;
|
|
173
|
+
} else {
|
|
174
|
+
removeIndices.add(oldIndex);
|
|
175
|
+
matchedPairs.push({ old: oldChild, new: null, oldIndex, newIndex: -1 });
|
|
176
|
+
oldIndex++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// Positional matching for unkeyed children
|
|
181
|
+
matchedPairs.push({ old: oldChild, new: newChild, oldIndex, newIndex });
|
|
182
|
+
oldIndex++;
|
|
183
|
+
newIndex++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Phase 2: Detect reordering using LIS ──────────────────────────────
|
|
188
|
+
|
|
189
|
+
const keyedPairs = matchedPairs.filter(
|
|
190
|
+
(p) => p.old !== null && p.new !== null && p.new.key !== null
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (keyedPairs.length > 0) {
|
|
194
|
+
const newIndices = keyedPairs.map((p) => p.newIndex);
|
|
195
|
+
const lis = longestIncreasingSubsequence(newIndices);
|
|
196
|
+
|
|
197
|
+
// Mark pairs that are part of LIS as stable, others need MOVE operations
|
|
198
|
+
const lisSet = new Set(lis.map((i) => keyedPairs[i].newIndex));
|
|
199
|
+
|
|
200
|
+
matchedPairs.forEach((pair) => {
|
|
201
|
+
if (pair.old !== null && pair.new !== null && pair.new.key !== null) {
|
|
202
|
+
if (!lisSet.has(pair.newIndex)) {
|
|
203
|
+
pair.needsMove = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ─── Phase 3: Generate operations ──────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
matchedPairs.forEach((pair) => {
|
|
212
|
+
context.stats.nodesCompared++;
|
|
213
|
+
|
|
214
|
+
if (pair.old === null && pair.new !== null) {
|
|
215
|
+
// Insert new child
|
|
216
|
+
operations.push(createDiffOp(INSERT, {
|
|
217
|
+
vnode: pair.new,
|
|
218
|
+
parentVNode: newVNode,
|
|
219
|
+
index: pair.newIndex,
|
|
220
|
+
}));
|
|
221
|
+
} else if (pair.old !== null && pair.new === null) {
|
|
222
|
+
// Remove old child
|
|
223
|
+
operations.push(createDiffOp(REMOVE, {
|
|
224
|
+
vnode: pair.old,
|
|
225
|
+
parentVNode: oldVNode,
|
|
226
|
+
index: pair.oldIndex,
|
|
227
|
+
}));
|
|
228
|
+
} else if (pair.old !== null && pair.new !== null) {
|
|
229
|
+
if (pair.needsMove) {
|
|
230
|
+
operations.push(createDiffOp(MOVE, {
|
|
231
|
+
vnode: pair.new,
|
|
232
|
+
oldVNode: pair.old,
|
|
233
|
+
fromIndex: pair.oldIndex,
|
|
234
|
+
toIndex: pair.newIndex,
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
// Reconcile the pair
|
|
238
|
+
reconcileElement(pair.old, pair.new, operations, context);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Reconcile two elements.
|
|
245
|
+
* @param {Object} oldVNode
|
|
246
|
+
* @param {Object} newVNode
|
|
247
|
+
* @param {Array} operations
|
|
248
|
+
* @param {Object} context
|
|
249
|
+
*/
|
|
250
|
+
function reconcileElement(oldVNode, newVNode, operations, context) {
|
|
251
|
+
if (shouldYield(context)) {
|
|
252
|
+
context.aborted = true;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Handle null cases
|
|
257
|
+
if (oldVNode === null && newVNode === null) return;
|
|
258
|
+
|
|
259
|
+
if (oldVNode === null) {
|
|
260
|
+
operations.push(createDiffOp(INSERT, { vnode: newVNode }));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (newVNode === null) {
|
|
265
|
+
operations.push(createDiffOp(REMOVE, { vnode: oldVNode }));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
context.stats.nodesCompared++;
|
|
270
|
+
|
|
271
|
+
// ─── Type Comparison ────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
if (!isSameType(oldVNode, newVNode)) {
|
|
274
|
+
operations.push(createDiffOp(REPLACE, {
|
|
275
|
+
oldVNode,
|
|
276
|
+
newVNode,
|
|
277
|
+
}));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─── shouldComponentUpdate optimization ────────────────────────────────
|
|
282
|
+
|
|
283
|
+
if (oldVNode.type && typeof oldVNode.type === 'function' && oldVNode.type._shouldComponentUpdate) {
|
|
284
|
+
if (!oldVNode.type._shouldComponentUpdate(oldVNode.props, newVNode.props)) {
|
|
285
|
+
operations.push(createDiffOp(SKIP, { vnode: newVNode }));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Memo component comparison ────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
if (oldVNode.type && oldVNode.type._isMemo && oldVNode.type._compare) {
|
|
293
|
+
if (oldVNode.type._compare(oldVNode.props, newVNode.props)) {
|
|
294
|
+
operations.push(createDiffOp(SKIP, { vnode: newVNode }));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─── Pure component comparison ────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
if (oldVNode.type && oldVNode.type._isPureComponent) {
|
|
302
|
+
if (shallowEqual(oldVNode.props, newVNode.props)) {
|
|
303
|
+
operations.push(createDiffOp(SKIP, { vnode: newVNode }));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ─── Text node comparison ─────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
if (oldVNode.type === '#text' && newVNode.type === '#text') {
|
|
311
|
+
if (oldVNode.props.nodeValue !== newVNode.props.nodeValue) {
|
|
312
|
+
operations.push(createDiffOp(UPDATE, {
|
|
313
|
+
vnode: newVNode,
|
|
314
|
+
changes: [{ type: 'set', key: 'nodeValue', value: newVNode.props.nodeValue }],
|
|
315
|
+
changeType: 'text',
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ─── Diff Props ────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
const propChanges = diffProps(oldVNode.props, newVNode.props);
|
|
324
|
+
if (propChanges.length > 0) {
|
|
325
|
+
operations.push(createDiffOp(UPDATE, {
|
|
326
|
+
vnode: newVNode,
|
|
327
|
+
changes: propChanges,
|
|
328
|
+
changeType: 'props',
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ─── Diff Style ────────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
const styleChanges = diffStyle(
|
|
335
|
+
oldVNode.props ? oldVNode.props.style : null,
|
|
336
|
+
newVNode.props ? newVNode.props.style : null
|
|
337
|
+
);
|
|
338
|
+
if (styleChanges.length > 0) {
|
|
339
|
+
operations.push(createDiffOp(UPDATE, {
|
|
340
|
+
vnode: newVNode,
|
|
341
|
+
changes: styleChanges,
|
|
342
|
+
changeType: 'style',
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─── Diff Event Handlers ────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
const eventChanges = diffEventHandlers(
|
|
349
|
+
oldVNode.props || {},
|
|
350
|
+
newVNode.props || {}
|
|
351
|
+
);
|
|
352
|
+
if (eventChanges.length > 0) {
|
|
353
|
+
operations.push(createDiffOp(UPDATE, {
|
|
354
|
+
vnode: newVNode,
|
|
355
|
+
changes: eventChanges,
|
|
356
|
+
changeType: 'events',
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ─── Diff Ref ──────────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
const refChange = diffRef(oldVNode.ref, newVNode.ref);
|
|
363
|
+
if (refChange) {
|
|
364
|
+
operations.push(createDiffOp(UPDATE, {
|
|
365
|
+
vnode: newVNode,
|
|
366
|
+
changes: [refChange],
|
|
367
|
+
changeType: 'ref',
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─── Diff dangerouslySetInnerHTML ──────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
const htmlChange = diffDangerouslySetInnerHTML(
|
|
374
|
+
oldVNode.props ? oldVNode.props.dangerouslySetInnerHTML : null,
|
|
375
|
+
newVNode.props ? newVNode.props.dangerouslySetInnerHTML : null
|
|
376
|
+
);
|
|
377
|
+
if (htmlChange) {
|
|
378
|
+
operations.push(createDiffOp(UPDATE, {
|
|
379
|
+
vnode: newVNode,
|
|
380
|
+
changes: [htmlChange],
|
|
381
|
+
changeType: 'innerHTML',
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ─── Recurse into Children ──────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
if (oldVNode.type !== '#text' && newVNode.type !== '#text') {
|
|
388
|
+
reconcileChildren(oldVNode, newVNode, operations, context);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ─── Key-Based Reconciliation Helpers ─────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Build a map of keys to child indices.
|
|
396
|
+
*/
|
|
397
|
+
function buildKeyMap(children) {
|
|
398
|
+
const map = new Map();
|
|
399
|
+
children.forEach((child, index) => {
|
|
400
|
+
if (child && child.key !== null) {
|
|
401
|
+
map.set(child.key, { child, index });
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
return map;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Normalize children from a vnode into a flat array.
|
|
409
|
+
*/
|
|
410
|
+
function normalizeChildren(vnode) {
|
|
411
|
+
if (!vnode) return [];
|
|
412
|
+
if (!vnode.props || !vnode.props.children) return [];
|
|
413
|
+
|
|
414
|
+
const children = vnode.props.children;
|
|
415
|
+
if (Array.isArray(children)) {
|
|
416
|
+
return children.filter((c) => c !== null && c !== undefined && typeof c !== 'boolean');
|
|
417
|
+
}
|
|
418
|
+
if (children !== null && children !== undefined && typeof children !== 'boolean') {
|
|
419
|
+
return [children];
|
|
420
|
+
}
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Check if two vnodes are the same type.
|
|
426
|
+
*/
|
|
427
|
+
function isSameType(oldVNode, newVNode) {
|
|
428
|
+
if (!isValidElement(oldVNode) || !isValidElement(newVNode)) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (oldVNode.type === newVNode.type) return true;
|
|
433
|
+
|
|
434
|
+
if (oldVNode.type === REACT_FRAGMENT_TYPE && newVNode.type === REACT_FRAGMENT_TYPE) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Compute the Longest Increasing Subsequence of an array.
|
|
443
|
+
* Used to minimize DOM moves during child reconciliation.
|
|
444
|
+
* @param {Array<number>} arr
|
|
445
|
+
* @returns {Array<number>} Indices of LIS elements
|
|
446
|
+
*/
|
|
447
|
+
function longestIncreasingSubsequence(arr) {
|
|
448
|
+
if (arr.length === 0) return [];
|
|
449
|
+
|
|
450
|
+
const dp = new Array(arr.length).fill(1);
|
|
451
|
+
const prev = new Array(arr.length).fill(-1);
|
|
452
|
+
|
|
453
|
+
for (let i = 1; i < arr.length; i++) {
|
|
454
|
+
for (let j = 0; j < i; j++) {
|
|
455
|
+
if (arr[j] < arr[i] && dp[j] + 1 > dp[i]) {
|
|
456
|
+
dp[i] = dp[j] + 1;
|
|
457
|
+
prev[i] = j;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Find the index of the maximum value in dp
|
|
463
|
+
let maxLen = 0;
|
|
464
|
+
let maxIdx = 0;
|
|
465
|
+
for (let i = 0; i < dp.length; i++) {
|
|
466
|
+
if (dp[i] > maxLen) {
|
|
467
|
+
maxLen = dp[i];
|
|
468
|
+
maxIdx = i;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Backtrack to find the LIS
|
|
473
|
+
const result = [];
|
|
474
|
+
let idx = maxIdx;
|
|
475
|
+
while (idx !== -1) {
|
|
476
|
+
result.unshift(idx);
|
|
477
|
+
idx = prev[idx];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ─── Prop Diffing ─────────────────────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Diff props between old and new vnodes.
|
|
487
|
+
*/
|
|
488
|
+
function diffProps(oldProps, newProps) {
|
|
489
|
+
const changes = [];
|
|
490
|
+
if (!oldProps) oldProps = {};
|
|
491
|
+
if (!newProps) newProps = {};
|
|
492
|
+
|
|
493
|
+
for (const key in newProps) {
|
|
494
|
+
if (!newProps.hasOwnProperty(key)) continue;
|
|
495
|
+
if (key === 'children' || key === 'style') continue;
|
|
496
|
+
if (key.startsWith('on')) continue;
|
|
497
|
+
if (key === 'dangerouslySetInnerHTML') continue;
|
|
498
|
+
|
|
499
|
+
if (!oldProps.hasOwnProperty(key)) {
|
|
500
|
+
changes.push({ type: 'set', key, value: newProps[key] });
|
|
501
|
+
} else if (!Object.is(oldProps[key], newProps[key])) {
|
|
502
|
+
changes.push({ type: 'set', key, value: newProps[key] });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
for (const key in oldProps) {
|
|
507
|
+
if (!oldProps.hasOwnProperty(key)) continue;
|
|
508
|
+
if (key === 'children' || key === 'style') continue;
|
|
509
|
+
if (key.startsWith('on')) continue;
|
|
510
|
+
if (key === 'dangerouslySetInnerHTML') continue;
|
|
511
|
+
|
|
512
|
+
if (!newProps.hasOwnProperty(key)) {
|
|
513
|
+
changes.push({ type: 'remove', key });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return changes;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ─── Style Diffing ────────────────────────────────────────────────────────────
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Diff style objects.
|
|
524
|
+
*/
|
|
525
|
+
function diffStyle(oldStyle, newStyle) {
|
|
526
|
+
const changes = [];
|
|
527
|
+
|
|
528
|
+
if (oldStyle === newStyle) return changes;
|
|
529
|
+
|
|
530
|
+
if (typeof oldStyle === 'string' && typeof newStyle === 'string') {
|
|
531
|
+
if (oldStyle !== newStyle) {
|
|
532
|
+
changes.push({ type: 'replace', value: newStyle });
|
|
533
|
+
}
|
|
534
|
+
return changes;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const oldObj = typeof oldStyle === 'object' ? oldStyle : {};
|
|
538
|
+
const newObj = typeof newStyle === 'object' ? newStyle : {};
|
|
539
|
+
|
|
540
|
+
for (const prop in newObj) {
|
|
541
|
+
if (!newObj.hasOwnProperty(prop)) continue;
|
|
542
|
+
|
|
543
|
+
if (!oldObj.hasOwnProperty(prop)) {
|
|
544
|
+
changes.push({ type: 'set', key: prop, value: newObj[prop] });
|
|
545
|
+
} else if (!Object.is(oldObj[prop], newObj[prop])) {
|
|
546
|
+
changes.push({ type: 'set', key: prop, value: newObj[prop] });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
for (const prop in oldObj) {
|
|
551
|
+
if (!oldObj.hasOwnProperty(prop)) continue;
|
|
552
|
+
if (!newObj.hasOwnProperty(prop)) {
|
|
553
|
+
changes.push({ type: 'remove', key: prop });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return changes;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ─── Event Handler Diffing ────────────────────────────────────────────────────
|
|
561
|
+
|
|
562
|
+
function diffEventHandlers(oldProps, newProps) {
|
|
563
|
+
const changes = [];
|
|
564
|
+
|
|
565
|
+
for (const key in newProps) {
|
|
566
|
+
if (!newProps.hasOwnProperty(key)) continue;
|
|
567
|
+
if (!key.startsWith('on')) continue;
|
|
568
|
+
|
|
569
|
+
if (!oldProps.hasOwnProperty(key) || oldProps[key] !== newProps[key]) {
|
|
570
|
+
changes.push({ type: 'set', key, value: newProps[key] });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (const key in oldProps) {
|
|
575
|
+
if (!oldProps.hasOwnProperty(key)) continue;
|
|
576
|
+
if (!key.startsWith('on')) continue;
|
|
577
|
+
|
|
578
|
+
if (!newProps.hasOwnProperty(key)) {
|
|
579
|
+
changes.push({ type: 'remove', key });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return changes;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ─── Ref Diffing ──────────────────────────────────────────────────────────────
|
|
587
|
+
|
|
588
|
+
function diffRef(oldRef, newRef) {
|
|
589
|
+
if (oldRef === newRef) return null;
|
|
590
|
+
|
|
591
|
+
if (oldRef && !newRef) {
|
|
592
|
+
return { type: 'remove', oldRef };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (!oldRef && newRef) {
|
|
596
|
+
return { type: 'set', newRef };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (oldRef !== newRef) {
|
|
600
|
+
return { type: 'replace', oldRef, newRef };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ─── dangerouslySetInnerHTML Diffing ──────────────────────────────────────────
|
|
607
|
+
|
|
608
|
+
function diffDangerouslySetInnerHTML(oldHtml, newHtml) {
|
|
609
|
+
if (oldHtml === newHtml) return null;
|
|
610
|
+
if (!oldHtml && !newHtml) return null;
|
|
611
|
+
|
|
612
|
+
const oldStr = oldHtml ? oldHtml.__html : '';
|
|
613
|
+
const newStr = newHtml ? newHtml.__html : '';
|
|
614
|
+
|
|
615
|
+
if (oldStr === newStr) return null;
|
|
616
|
+
|
|
617
|
+
return { type: 'set', key: 'dangerouslySetInnerHTML', value: newHtml };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ─── ShouldComponentUpdate Optimization ────────────────────────────────────────
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Create a memo component.
|
|
624
|
+
*/
|
|
625
|
+
function memo(component, areEqual) {
|
|
626
|
+
const memoComponent = function MemoComponent(props) {
|
|
627
|
+
return component(props);
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
memoComponent._isMemo = true;
|
|
631
|
+
memoComponent._compare = areEqual || shallowEqual;
|
|
632
|
+
memoComponent.displayName = 'Memo(' + (component.displayName || component.name || 'Anonymous') + ')';
|
|
633
|
+
|
|
634
|
+
return memoComponent;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Shallow equality comparison.
|
|
639
|
+
*/
|
|
640
|
+
function shallowEqual(objA, objB) {
|
|
641
|
+
if (Object.is(objA, objB)) return true;
|
|
642
|
+
|
|
643
|
+
if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const keysA = Object.keys(objA);
|
|
648
|
+
const keysB = Object.keys(objB);
|
|
649
|
+
|
|
650
|
+
if (keysA.length !== keysB.length) return false;
|
|
651
|
+
|
|
652
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
653
|
+
if (!Object.is(objA[keysA[i]], objB[keysA[i]])) return false;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
660
|
+
|
|
661
|
+
module.exports = {
|
|
662
|
+
reconcile,
|
|
663
|
+
reconcileChildren,
|
|
664
|
+
reconcileElement,
|
|
665
|
+
diffProps,
|
|
666
|
+
diffStyle,
|
|
667
|
+
diffEventHandlers,
|
|
668
|
+
diffRef,
|
|
669
|
+
diffDangerouslySetInnerHTML,
|
|
670
|
+
isSameType,
|
|
671
|
+
buildKeyMap,
|
|
672
|
+
normalizeChildren,
|
|
673
|
+
memo,
|
|
674
|
+
shallowEqual,
|
|
675
|
+
createFiber,
|
|
676
|
+
createDiffOp,
|
|
677
|
+
longestIncreasingSubsequence,
|
|
678
|
+
shouldYield,
|
|
679
|
+
INSERT,
|
|
680
|
+
MOVE,
|
|
681
|
+
REMOVE,
|
|
682
|
+
UPDATE,
|
|
683
|
+
REPLACE,
|
|
684
|
+
SKIP,
|
|
685
|
+
REORDER,
|
|
686
|
+
RECONCILE_PRIORITY,
|
|
687
|
+
};
|