@wsxjs/wsx-core 0.0.23 → 0.0.25
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/dist/{chunk-ESZYREJK.mjs → chunk-5Q2VEEUH.mjs} +160 -35
- package/dist/index.js +226 -64
- package/dist/index.mjs +67 -30
- package/dist/jsx-runtime.js +160 -35
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +160 -35
- package/dist/jsx.mjs +1 -1
- package/package.json +2 -2
- package/src/base-component.ts +27 -0
- package/src/light-component.ts +20 -8
- package/src/render-context.ts +4 -0
- package/src/utils/cache-key.ts +27 -21
- package/src/utils/element-creation.ts +5 -0
- package/src/utils/element-update.ts +122 -45
- package/src/utils/update-children-helpers.ts +184 -18
- package/src/web-component.ts +72 -41
- package/dist/chunk-BPQGLNOQ.mjs +0 -1140
- package/dist/chunk-OGMB43J4.mjs +0 -1131
- package/dist/chunk-OXFZ575O.mjs +0 -1091
- package/dist/chunk-TKHKPLBM.mjs +0 -1142
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
updateOrCreateTextNode,
|
|
18
18
|
removeNodeIfNotPreserved,
|
|
19
19
|
replaceOrInsertElement,
|
|
20
|
+
replaceOrInsertElementAtPosition,
|
|
20
21
|
appendNewChild,
|
|
21
22
|
buildNewChildrenMaps,
|
|
22
23
|
deduplicateCacheKeys,
|
|
@@ -39,7 +40,16 @@ function removeProp(
|
|
|
39
40
|
|
|
40
41
|
// 处理特殊属性
|
|
41
42
|
if (key === "ref") {
|
|
42
|
-
// ref
|
|
43
|
+
// 关键修复:当 ref 被移除时,调用回调并传入 null
|
|
44
|
+
// 这确保组件可以清理引用,避免使用已移除的元素
|
|
45
|
+
// 例如:LanguageSwitcher 的 dropdownElement 应该在元素被移除时设置为 null
|
|
46
|
+
if (typeof oldValue === "function") {
|
|
47
|
+
try {
|
|
48
|
+
oldValue(null);
|
|
49
|
+
} catch {
|
|
50
|
+
// 忽略回调错误
|
|
51
|
+
}
|
|
52
|
+
}
|
|
43
53
|
return;
|
|
44
54
|
}
|
|
45
55
|
|
|
@@ -58,8 +68,17 @@ function removeProp(
|
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
if (key.startsWith("on") && typeof oldValue === "function") {
|
|
61
|
-
//
|
|
62
|
-
|
|
71
|
+
// 关键修复:移除事件监听器,避免内存泄漏
|
|
72
|
+
const eventName = key.slice(2).toLowerCase();
|
|
73
|
+
const listenerKey = `__wsxListener_${eventName}`;
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
+
const savedListener = (element as any)[listenerKey];
|
|
76
|
+
|
|
77
|
+
if (savedListener) {
|
|
78
|
+
element.removeEventListener(eventName, savedListener);
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
|
+
delete (element as any)[listenerKey];
|
|
81
|
+
}
|
|
63
82
|
return;
|
|
64
83
|
}
|
|
65
84
|
|
|
@@ -134,9 +153,21 @@ function applySingleProp(
|
|
|
134
153
|
// 处理事件监听器
|
|
135
154
|
if (key.startsWith("on") && typeof value === "function") {
|
|
136
155
|
const eventName = key.slice(2).toLowerCase();
|
|
137
|
-
|
|
138
|
-
//
|
|
156
|
+
|
|
157
|
+
// 关键修复:移除旧的监听器,避免重复添加
|
|
158
|
+
// 在元素上保存监听器引用,以便后续移除
|
|
159
|
+
const listenerKey = `__wsxListener_${eventName}`;
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
161
|
+
const oldListener = (element as any)[listenerKey];
|
|
162
|
+
|
|
163
|
+
if (oldListener) {
|
|
164
|
+
element.removeEventListener(eventName, oldListener);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 添加新监听器并保存引用
|
|
139
168
|
element.addEventListener(eventName, value as EventListener);
|
|
169
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
170
|
+
(element as any)[listenerKey] = value;
|
|
140
171
|
return;
|
|
141
172
|
}
|
|
142
173
|
|
|
@@ -252,7 +283,7 @@ export function updateChildren(
|
|
|
252
283
|
element: HTMLElement | SVGElement,
|
|
253
284
|
oldChildren: JSXChildren[],
|
|
254
285
|
newChildren: JSXChildren[],
|
|
255
|
-
|
|
286
|
+
_cacheManager?: DOMCacheManager // 可选参数,保留以保持 API 兼容性
|
|
256
287
|
): void {
|
|
257
288
|
const flatOld = flattenChildrenSafe(oldChildren);
|
|
258
289
|
const flatNew = flattenChildrenSafe(newChildren);
|
|
@@ -263,6 +294,8 @@ export function updateChildren(
|
|
|
263
294
|
// 更新现有子节点
|
|
264
295
|
const minLength = Math.min(flatOld.length, flatNew.length);
|
|
265
296
|
const domIndex = { value: 0 }; // 使用对象包装,使其可在函数间传递
|
|
297
|
+
// 跟踪已处理的节点,用于确定正确的位置
|
|
298
|
+
const processedNodes = new Set<Node>();
|
|
266
299
|
|
|
267
300
|
for (let i = 0; i < minLength; i++) {
|
|
268
301
|
const oldChild = flatOld[i];
|
|
@@ -284,13 +317,20 @@ export function updateChildren(
|
|
|
284
317
|
}
|
|
285
318
|
} else if (typeof oldChild === "string" || typeof oldChild === "number") {
|
|
286
319
|
oldNode = findTextNode(element, domIndex);
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
320
|
+
// RFC-0044 修复:fallback 搜索必须验证文本内容
|
|
321
|
+
// 在缓存元素复用场景下,不能盲目返回第一个找到的文本节点
|
|
322
|
+
// 必须确保内容匹配,否则会导致错误的更新
|
|
290
323
|
if (!oldNode && element.childNodes.length > 0) {
|
|
324
|
+
const oldText = String(oldChild);
|
|
291
325
|
for (let j = domIndex.value; j < element.childNodes.length; j++) {
|
|
292
326
|
const node = element.childNodes[j];
|
|
293
|
-
|
|
327
|
+
// 关键修复:只检查直接子文本节点,确保 node.parentNode === element
|
|
328
|
+
// 并且必须验证文本内容是否匹配
|
|
329
|
+
if (
|
|
330
|
+
node.nodeType === Node.TEXT_NODE &&
|
|
331
|
+
node.parentNode === element &&
|
|
332
|
+
node.textContent === oldText
|
|
333
|
+
) {
|
|
294
334
|
oldNode = node;
|
|
295
335
|
// 更新 domIndex 到找到的文本节点之后
|
|
296
336
|
domIndex.value = j + 1;
|
|
@@ -316,10 +356,19 @@ export function updateChildren(
|
|
|
316
356
|
oldNode.textContent !== newText);
|
|
317
357
|
|
|
318
358
|
if (needsUpdate) {
|
|
319
|
-
|
|
359
|
+
// RFC-0044 修复:使用返回的节点引用直接标记
|
|
360
|
+
// updateOrCreateTextNode 现在返回更新/创建的节点
|
|
361
|
+
// 这样可以准确标记,避免搜索失败导致的重复创建
|
|
362
|
+
const updatedNode = updateOrCreateTextNode(element, oldNode, newText);
|
|
363
|
+
if (updatedNode && !processedNodes.has(updatedNode)) {
|
|
364
|
+
processedNodes.add(updatedNode);
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
// 即使不需要更新,也要标记为已处理(文本节点已存在且内容正确)
|
|
368
|
+
if (oldNode && oldNode.parentNode === element) {
|
|
369
|
+
processedNodes.add(oldNode);
|
|
370
|
+
}
|
|
320
371
|
}
|
|
321
|
-
// 如果文本内容相同且 oldNode 为 null,不需要做任何操作
|
|
322
|
-
// 因为文本节点可能已经存在于 DOM 中且内容正确,或者不需要创建
|
|
323
372
|
} else {
|
|
324
373
|
// 类型变化:文本 -> 元素/Fragment
|
|
325
374
|
removeNodeIfNotPreserved(element, oldNode);
|
|
@@ -336,47 +385,74 @@ export function updateChildren(
|
|
|
336
385
|
continue; // 跳过保留的元素
|
|
337
386
|
}
|
|
338
387
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
388
|
+
if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
389
|
+
// 关键修复:确定正确的位置
|
|
390
|
+
// 策略:基于 flatNew 数组的顺序来确定位置,而不是 DOM 中的实际顺序
|
|
391
|
+
// 因为某些元素可能被隐藏(position: absolute),导致 DOM 顺序与数组顺序不同
|
|
392
|
+
|
|
393
|
+
// 找到索引 i 之前的所有元素,确定 newChild 应该在这些元素之后
|
|
394
|
+
let targetNextSibling: Node | null = null;
|
|
395
|
+
let foundPreviousElement = false;
|
|
396
|
+
|
|
397
|
+
// 从后往前查找,找到最后一个在 DOM 中的前一个元素
|
|
398
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
399
|
+
const prevChild = flatNew[j];
|
|
400
|
+
if (prevChild instanceof HTMLElement || prevChild instanceof SVGElement) {
|
|
401
|
+
if (prevChild.parentNode === element) {
|
|
402
|
+
// 找到前一个元素,newChild 应该在它之后
|
|
403
|
+
targetNextSibling = prevChild.nextSibling;
|
|
404
|
+
foundPreviousElement = true;
|
|
405
|
+
break;
|
|
357
406
|
}
|
|
358
407
|
}
|
|
359
|
-
} else {
|
|
360
|
-
// 如果没有 cacheManager,假设 h() 已经更新了子元素
|
|
361
|
-
if (oldNode === newChild && newChild.parentNode === element) {
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
408
|
}
|
|
365
|
-
// 如果位置不对,需要调整
|
|
366
|
-
}
|
|
367
409
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
410
|
+
// 如果没有找到前一个元素,newChild 应该在开头
|
|
411
|
+
if (!foundPreviousElement) {
|
|
412
|
+
// 找到第一个非保留、未处理的子节点
|
|
413
|
+
const firstChild = Array.from(element.childNodes).find(
|
|
414
|
+
(node) => !shouldPreserveElement(node) && !processedNodes.has(node)
|
|
415
|
+
);
|
|
416
|
+
targetNextSibling = firstChild || null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 检查 newChild 是否已经在正确位置
|
|
420
|
+
const isInCorrectPosition =
|
|
421
|
+
newChild.parentNode === element && newChild.nextSibling === targetNextSibling;
|
|
422
|
+
|
|
423
|
+
// 如果 newChild === oldChild 且位置正确,说明是同一个元素且位置正确
|
|
424
|
+
if (newChild === oldChild && isInCorrectPosition) {
|
|
425
|
+
// 标记为已处理
|
|
426
|
+
if (oldNode) processedNodes.add(oldNode);
|
|
427
|
+
processedNodes.add(newChild);
|
|
371
428
|
continue; // 元素已经在正确位置,不需要更新
|
|
372
429
|
}
|
|
373
|
-
|
|
430
|
+
|
|
431
|
+
// 如果 newChild 是从缓存复用的(与 oldChild 不同),或者位置不对,需要调整
|
|
432
|
+
// 使用 oldNode 作为参考(如果存在),但目标位置基于数组顺序
|
|
433
|
+
const referenceNode = oldNode && oldNode.parentNode === element ? oldNode : null;
|
|
434
|
+
replaceOrInsertElementAtPosition(
|
|
435
|
+
element,
|
|
436
|
+
newChild,
|
|
437
|
+
referenceNode,
|
|
438
|
+
targetNextSibling
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// 关键修复:在替换元素后,从 processedNodes 中移除旧的元素引用
|
|
442
|
+
// 这样可以防止旧的 span 元素内部的文本节点被误判为不应该移除
|
|
443
|
+
if (oldNode && oldNode !== newChild) {
|
|
444
|
+
processedNodes.delete(oldNode);
|
|
445
|
+
}
|
|
446
|
+
// 标记新元素为已处理
|
|
447
|
+
processedNodes.add(newChild);
|
|
374
448
|
} else {
|
|
375
449
|
// 类型变化:元素 -> 文本/Fragment
|
|
376
450
|
removeNodeIfNotPreserved(element, oldNode);
|
|
377
451
|
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
378
452
|
const newTextNode = document.createTextNode(String(newChild));
|
|
379
453
|
element.insertBefore(newTextNode, oldNode?.nextSibling || null);
|
|
454
|
+
// 关键修复:标记新创建的文本节点为已处理,防止在移除阶段被误删
|
|
455
|
+
processedNodes.add(newTextNode);
|
|
380
456
|
} else if (newChild instanceof DocumentFragment) {
|
|
381
457
|
element.insertBefore(newChild, oldNode?.nextSibling || null);
|
|
382
458
|
}
|
|
@@ -386,7 +462,7 @@ export function updateChildren(
|
|
|
386
462
|
|
|
387
463
|
// 添加新子节点
|
|
388
464
|
for (let i = minLength; i < flatNew.length; i++) {
|
|
389
|
-
appendNewChild(element, flatNew[i]);
|
|
465
|
+
appendNewChild(element, flatNew[i], processedNodes);
|
|
390
466
|
}
|
|
391
467
|
|
|
392
468
|
// 移除多余子节点(使用纯函数简化逻辑)
|
|
@@ -397,10 +473,11 @@ export function updateChildren(
|
|
|
397
473
|
deduplicateCacheKeys(element, cacheKeyMap);
|
|
398
474
|
|
|
399
475
|
// 步骤 3: 收集需要移除的节点(跳过保留元素和新子元素)
|
|
400
|
-
const nodesToRemove = collectNodesToRemove(element, elementSet, cacheKeyMap);
|
|
476
|
+
const nodesToRemove = collectNodesToRemove(element, elementSet, cacheKeyMap, processedNodes);
|
|
401
477
|
|
|
402
478
|
// 步骤 4: 批量移除节点(从后往前,避免索引变化)
|
|
403
|
-
|
|
479
|
+
// 传递 cacheManager 以便在移除元素时调用 ref 回调
|
|
480
|
+
removeNodes(element, nodesToRemove, _cacheManager);
|
|
404
481
|
|
|
405
482
|
// 步骤 5: 重新插入所有保留的元素到 DOM 末尾
|
|
406
483
|
// 这确保了第三方库注入的元素不会丢失
|
|
@@ -81,7 +81,9 @@ export function findTextNode(
|
|
|
81
81
|
): Node | null {
|
|
82
82
|
while (domIndex.value < parent.childNodes.length) {
|
|
83
83
|
const node = parent.childNodes[domIndex.value];
|
|
84
|
-
|
|
84
|
+
// 关键修复:只检查直接子文本节点,确保 node.parentNode === parent
|
|
85
|
+
// 这样可以防止将元素内部的文本节点(如 span 内部的文本节点)误判为独立的文本节点
|
|
86
|
+
if (node.nodeType === Node.TEXT_NODE && node.parentNode === parent) {
|
|
85
87
|
const textNode = node;
|
|
86
88
|
domIndex.value++;
|
|
87
89
|
return textNode;
|
|
@@ -110,29 +112,47 @@ export function shouldUpdateTextNode(
|
|
|
110
112
|
|
|
111
113
|
/**
|
|
112
114
|
* 更新或创建文本节点
|
|
115
|
+
* @returns 更新后的文本节点(用于标记为已处理)
|
|
113
116
|
*/
|
|
114
117
|
export function updateOrCreateTextNode(
|
|
115
118
|
parent: HTMLElement | SVGElement,
|
|
116
119
|
oldNode: Node | null,
|
|
117
120
|
newText: string
|
|
118
|
-
):
|
|
121
|
+
): Node {
|
|
119
122
|
if (oldNode && oldNode.nodeType === Node.TEXT_NODE) {
|
|
120
123
|
// 只有当文本内容不同时才更新
|
|
121
124
|
if (oldNode.textContent !== newText) {
|
|
122
125
|
oldNode.textContent = newText;
|
|
123
126
|
}
|
|
127
|
+
return oldNode;
|
|
124
128
|
} else {
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
// RFC 0048 关键修复:如果 oldNode 为 null,先检查是否已经存在相同内容的直接子文本节点
|
|
130
|
+
// 这样可以防止重复创建文本节点
|
|
131
|
+
// 只检查直接子文本节点,不检查元素内部的文本节点
|
|
132
|
+
if (!oldNode) {
|
|
133
|
+
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
134
|
+
const node = parent.childNodes[i];
|
|
135
|
+
// 关键修复:只检查直接子文本节点,确保 node.parentNode === parent
|
|
136
|
+
// 这样可以防止将元素内部的文本节点(如 span 内部的文本节点)误判为独立的文本节点
|
|
137
|
+
if (
|
|
138
|
+
node.nodeType === Node.TEXT_NODE &&
|
|
139
|
+
node.parentNode === parent &&
|
|
140
|
+
node.textContent === newText
|
|
141
|
+
) {
|
|
142
|
+
// 找到相同内容的直接子文本节点,返回它而不是创建新节点
|
|
143
|
+
return node;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 如果没有找到相同内容的文本节点,创建新节点
|
|
130
149
|
const newTextNode = document.createTextNode(newText);
|
|
131
150
|
if (oldNode && !shouldPreserveElement(oldNode)) {
|
|
132
151
|
parent.replaceChild(newTextNode, oldNode);
|
|
133
152
|
} else {
|
|
134
153
|
parent.insertBefore(newTextNode, oldNode || null);
|
|
135
154
|
}
|
|
155
|
+
return newTextNode;
|
|
136
156
|
}
|
|
137
157
|
}
|
|
138
158
|
|
|
@@ -155,38 +175,134 @@ export function replaceOrInsertElement(
|
|
|
155
175
|
parent: HTMLElement | SVGElement,
|
|
156
176
|
newChild: HTMLElement | SVGElement,
|
|
157
177
|
oldNode: Node | null
|
|
178
|
+
): void {
|
|
179
|
+
// 确定目标位置:
|
|
180
|
+
// - 如果 oldNode 是保留元素,应该在 oldNode 之前插入(targetNextSibling = oldNode)
|
|
181
|
+
// - 否则,应该在 oldNode 的位置(oldNode.nextSibling 之前)
|
|
182
|
+
const targetNextSibling =
|
|
183
|
+
oldNode && shouldPreserveElement(oldNode) ? oldNode : oldNode?.nextSibling || null;
|
|
184
|
+
replaceOrInsertElementAtPosition(parent, newChild, oldNode, targetNextSibling);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 替换或插入元素节点到指定位置
|
|
189
|
+
* 确保元素在正确的位置,保持 DOM 顺序
|
|
190
|
+
*/
|
|
191
|
+
export function replaceOrInsertElementAtPosition(
|
|
192
|
+
parent: HTMLElement | SVGElement,
|
|
193
|
+
newChild: HTMLElement | SVGElement,
|
|
194
|
+
oldNode: Node | null,
|
|
195
|
+
targetNextSibling: Node | null
|
|
158
196
|
): void {
|
|
159
197
|
// 如果新元素已经在其他父元素中,先移除
|
|
160
198
|
if (newChild.parentNode && newChild.parentNode !== parent) {
|
|
161
199
|
newChild.parentNode.removeChild(newChild);
|
|
162
200
|
}
|
|
163
201
|
|
|
164
|
-
|
|
202
|
+
// 检查元素是否已经在正确位置
|
|
203
|
+
const isInCorrectPosition =
|
|
204
|
+
newChild.parentNode === parent && newChild.nextSibling === targetNextSibling;
|
|
205
|
+
|
|
206
|
+
if (isInCorrectPosition) {
|
|
207
|
+
// 已经在正确位置,不需要移动
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 如果元素已经在 parent 中但位置不对,需要移动到正确位置
|
|
212
|
+
if (newChild.parentNode === parent) {
|
|
213
|
+
// insertBefore 会自动处理:如果元素已经在 DOM 中,会先移除再插入到新位置
|
|
214
|
+
parent.insertBefore(newChild, targetNextSibling);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 元素不在 parent 中,需要插入或替换
|
|
219
|
+
if (oldNode && oldNode.parentNode === parent && !shouldPreserveElement(oldNode)) {
|
|
165
220
|
if (oldNode !== newChild) {
|
|
221
|
+
// 关键修复:在替换元素之前,标记旧元素内的所有文本节点为已处理
|
|
222
|
+
// 这样可以防止在移除阶段被误删
|
|
223
|
+
// 注意:这里只标记直接子文本节点,不递归处理嵌套元素内的文本节点
|
|
224
|
+
// 因为 replaceChild 会一起处理元素及其所有子节点
|
|
166
225
|
parent.replaceChild(newChild, oldNode);
|
|
167
226
|
}
|
|
168
|
-
} else
|
|
169
|
-
|
|
227
|
+
} else {
|
|
228
|
+
// RFC 0048 关键修复:在插入元素之前,检查是否已经存在相同内容的元素
|
|
229
|
+
// 如果 newChild 已经在 parent 中,不应该再次插入
|
|
230
|
+
// 如果 parent 中已经存在相同标签名和内容的元素,也不应该插入
|
|
231
|
+
// 这样可以防止重复插入相同的元素(特别是从 HTML 字符串解析而来的元素)
|
|
232
|
+
if (newChild.parentNode === parent) {
|
|
233
|
+
// 元素已经在 parent 中,不需要再次插入
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// RFC 0048 关键修复:检查是否已经存在相同内容的元素
|
|
237
|
+
// 注意:这个检查只适用于从 HTML 字符串解析而来的元素(没有 __wsxCacheKey)
|
|
238
|
+
// 对于由 h() 创建的元素(有 __wsxCacheKey),应该通过引用匹配,而不是内容匹配
|
|
239
|
+
// 这样可以避免误判语言切换器等组件(它们由 h() 创建,每次渲染可能有相同内容但不同引用)
|
|
240
|
+
const newChildCacheKey = getElementCacheKey(newChild);
|
|
241
|
+
// 只有当 newChild 没有 cache key 时,才进行内容匹配检查
|
|
242
|
+
// 如果有 cache key,说明是由 h() 创建的,应该通过引用匹配(已经在 elementSet 中检查)
|
|
243
|
+
if (!newChildCacheKey) {
|
|
244
|
+
const newChildContent = newChild.textContent || "";
|
|
245
|
+
const newChildTag = newChild.tagName.toLowerCase();
|
|
246
|
+
// 检查 parent 中是否已经存在相同标签名和内容的元素(且也没有 cache key)
|
|
247
|
+
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
248
|
+
const existingNode = parent.childNodes[i];
|
|
249
|
+
if (existingNode instanceof HTMLElement || existingNode instanceof SVGElement) {
|
|
250
|
+
const existingCacheKey = getElementCacheKey(existingNode);
|
|
251
|
+
// 只有当 existingNode 也没有 cache key 时,才进行内容匹配
|
|
252
|
+
if (
|
|
253
|
+
!existingCacheKey &&
|
|
254
|
+
existingNode.tagName.toLowerCase() === newChildTag &&
|
|
255
|
+
existingNode.textContent === newChildContent &&
|
|
256
|
+
existingNode !== newChild
|
|
257
|
+
) {
|
|
258
|
+
// 找到相同内容的元素(且都没有 cache key),不需要插入 newChild
|
|
259
|
+
// 这是从 HTML 字符串解析而来的重复元素
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// 插入到目标位置
|
|
266
|
+
parent.insertBefore(newChild, targetNextSibling);
|
|
170
267
|
}
|
|
171
268
|
}
|
|
172
269
|
|
|
173
270
|
/**
|
|
174
271
|
* 添加新的子节点到末尾
|
|
175
272
|
*/
|
|
176
|
-
export function appendNewChild(
|
|
273
|
+
export function appendNewChild(
|
|
274
|
+
parent: HTMLElement | SVGElement,
|
|
275
|
+
child: JSXChildren,
|
|
276
|
+
processedNodes?: Set<Node>
|
|
277
|
+
): void {
|
|
177
278
|
if (child === null || child === undefined || child === false) {
|
|
178
279
|
return;
|
|
179
280
|
}
|
|
180
281
|
|
|
181
282
|
if (typeof child === "string" || typeof child === "number") {
|
|
182
|
-
|
|
283
|
+
const newTextNode = document.createTextNode(String(child));
|
|
284
|
+
parent.appendChild(newTextNode);
|
|
285
|
+
// 关键修复:标记新创建的文本节点为已处理,防止在移除阶段被误删
|
|
286
|
+
if (processedNodes) {
|
|
287
|
+
processedNodes.add(newTextNode);
|
|
288
|
+
}
|
|
183
289
|
} else if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
290
|
+
// RFC 0048 关键修复:在插入元素之前,检查是否已经存在相同的元素
|
|
291
|
+
// 如果 child 已经在 parent 中,不应该再次插入
|
|
292
|
+
// 这样可以防止重复插入相同的元素(特别是从 HTML 字符串解析而来的元素)
|
|
293
|
+
if (child.parentNode === parent) {
|
|
294
|
+
// 元素已经在 parent 中,不需要再次插入
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
184
297
|
// 确保元素不在其他父元素中
|
|
185
298
|
if (child.parentNode && child.parentNode !== parent) {
|
|
186
299
|
child.parentNode.removeChild(child);
|
|
187
300
|
}
|
|
188
|
-
|
|
189
|
-
|
|
301
|
+
// 插入到末尾
|
|
302
|
+
parent.appendChild(child);
|
|
303
|
+
// RFC 0048 关键修复:标记新添加的元素为已处理,防止在移除阶段被误删
|
|
304
|
+
if (processedNodes) {
|
|
305
|
+
processedNodes.add(child);
|
|
190
306
|
}
|
|
191
307
|
} else if (child instanceof DocumentFragment) {
|
|
192
308
|
parent.appendChild(child);
|
|
@@ -228,13 +344,34 @@ export function buildNewChildrenMaps(flatNew: JSXChildren[]): {
|
|
|
228
344
|
export function shouldRemoveNode(
|
|
229
345
|
node: Node,
|
|
230
346
|
elementSet: Set<HTMLElement | SVGElement | DocumentFragment>,
|
|
231
|
-
cacheKeyMap: Map<string, HTMLElement | SVGElement
|
|
347
|
+
cacheKeyMap: Map<string, HTMLElement | SVGElement>,
|
|
348
|
+
processedNodes?: Set<Node>
|
|
232
349
|
): boolean {
|
|
233
350
|
// 保留的元素不移除
|
|
234
351
|
if (shouldPreserveElement(node)) {
|
|
235
352
|
return false;
|
|
236
353
|
}
|
|
237
354
|
|
|
355
|
+
// 关键修复:如果文本节点已被处理,不应该移除
|
|
356
|
+
if (node.nodeType === Node.TEXT_NODE && processedNodes && processedNodes.has(node)) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 关键修复:如果文本节点在已处理的元素内部,不应该移除
|
|
361
|
+
// 这样可以防止在元素被替换时,元素内部的文本节点被误删
|
|
362
|
+
// 但是,只有当父元素还在当前 parent 的子树中时才检查
|
|
363
|
+
if (node.nodeType === Node.TEXT_NODE && processedNodes) {
|
|
364
|
+
let parent = node.parentNode;
|
|
365
|
+
while (parent) {
|
|
366
|
+
// 关键修复:只有当父元素还在 DOM 中并且在当前 parent 的子树中时才检查
|
|
367
|
+
// 因为如果父元素被 replaceChild 移除了,它就不在 DOM 中了
|
|
368
|
+
if (processedNodes.has(parent) && parent.parentNode) {
|
|
369
|
+
return false; // 文本节点在已处理的元素内部,不应该移除
|
|
370
|
+
}
|
|
371
|
+
parent = parent.parentNode;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
238
375
|
// 检查是否在新子元素集合中(通过引用)
|
|
239
376
|
if (
|
|
240
377
|
node instanceof HTMLElement ||
|
|
@@ -283,6 +420,14 @@ export function deduplicateCacheKeys(
|
|
|
283
420
|
if (child !== newChild) {
|
|
284
421
|
parent.replaceChild(newChild, child);
|
|
285
422
|
}
|
|
423
|
+
// 如果 child === newChild,说明 newChild 已经在正确位置,不需要操作
|
|
424
|
+
} else if (cacheKey && cacheKeyMap.has(cacheKey) && processedCacheKeys.has(cacheKey)) {
|
|
425
|
+
// 如果 cache key 已经被处理过,说明有重复,应该移除这个旧元素
|
|
426
|
+
// 但是,只有当 child 不是 newChild 时才移除
|
|
427
|
+
const newChild = cacheKeyMap.get(cacheKey)!;
|
|
428
|
+
if (child !== newChild) {
|
|
429
|
+
parent.removeChild(child);
|
|
430
|
+
}
|
|
286
431
|
}
|
|
287
432
|
}
|
|
288
433
|
}
|
|
@@ -294,13 +439,14 @@ export function deduplicateCacheKeys(
|
|
|
294
439
|
export function collectNodesToRemove(
|
|
295
440
|
parent: HTMLElement | SVGElement,
|
|
296
441
|
elementSet: Set<HTMLElement | SVGElement | DocumentFragment>,
|
|
297
|
-
cacheKeyMap: Map<string, HTMLElement | SVGElement
|
|
442
|
+
cacheKeyMap: Map<string, HTMLElement | SVGElement>,
|
|
443
|
+
processedNodes?: Set<Node>
|
|
298
444
|
): Node[] {
|
|
299
445
|
const nodesToRemove: Node[] = [];
|
|
300
446
|
|
|
301
447
|
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
302
448
|
const node = parent.childNodes[i];
|
|
303
|
-
if (shouldRemoveNode(node, elementSet, cacheKeyMap)) {
|
|
449
|
+
if (shouldRemoveNode(node, elementSet, cacheKeyMap, processedNodes)) {
|
|
304
450
|
nodesToRemove.push(node);
|
|
305
451
|
}
|
|
306
452
|
}
|
|
@@ -310,11 +456,31 @@ export function collectNodesToRemove(
|
|
|
310
456
|
|
|
311
457
|
/**
|
|
312
458
|
* 批量移除节点(从后往前,避免索引变化)
|
|
459
|
+
* @param parent - 父元素
|
|
460
|
+
* @param nodes - 要移除的节点列表
|
|
461
|
+
* @param cacheManager - 可选的缓存管理器,用于获取元素的 ref 回调
|
|
313
462
|
*/
|
|
314
|
-
export function removeNodes(
|
|
463
|
+
export function removeNodes(
|
|
464
|
+
parent: HTMLElement | SVGElement,
|
|
465
|
+
nodes: Node[],
|
|
466
|
+
cacheManager?: { getMetadata: (element: Element) => Record<string, unknown> | undefined }
|
|
467
|
+
): void {
|
|
315
468
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
316
469
|
const node = nodes[i];
|
|
317
470
|
if (node.parentNode === parent) {
|
|
471
|
+
// 关键修复:在移除元素之前,检查是否有 ref 回调,如果有,调用它并传入 null
|
|
472
|
+
// 这确保组件可以清理引用,避免使用已移除的元素
|
|
473
|
+
if (cacheManager && (node instanceof HTMLElement || node instanceof SVGElement)) {
|
|
474
|
+
const metadata = cacheManager.getMetadata(node);
|
|
475
|
+
const refCallback = metadata?.ref;
|
|
476
|
+
if (typeof refCallback === "function") {
|
|
477
|
+
try {
|
|
478
|
+
refCallback(null);
|
|
479
|
+
} catch {
|
|
480
|
+
// 忽略回调错误
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
318
484
|
parent.removeChild(node);
|
|
319
485
|
}
|
|
320
486
|
}
|