@wsxjs/wsx-core 0.0.27 → 0.0.28
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-34PNC5CJ.mjs +1307 -0
- package/dist/chunk-HWJ7GZD6.mjs +1327 -0
- package/dist/chunk-ZY36MEHX.mjs +1306 -0
- package/dist/index.js +133 -141
- package/dist/index.mjs +3 -3
- package/dist/jsx-runtime.js +131 -139
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +131 -139
- package/dist/jsx.mjs +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/jsx-factory.ts +3 -1
- package/src/utils/dom-utils.ts +19 -0
- package/src/utils/element-creation.ts +3 -1
- package/src/utils/element-marking.ts +4 -2
- package/src/utils/element-update.ts +99 -126
- package/src/utils/update-children-helpers.ts +95 -55
- package/src/web-component.ts +6 -5
|
@@ -16,8 +16,6 @@ import {
|
|
|
16
16
|
findTextNode,
|
|
17
17
|
updateOrCreateTextNode,
|
|
18
18
|
removeNodeIfNotPreserved,
|
|
19
|
-
replaceOrInsertElement,
|
|
20
|
-
replaceOrInsertElementAtPosition,
|
|
21
19
|
appendNewChild,
|
|
22
20
|
buildNewChildrenMaps,
|
|
23
21
|
deduplicateCacheKeys,
|
|
@@ -324,7 +322,8 @@ export function updateChildren(
|
|
|
324
322
|
|
|
325
323
|
// 更新现有子节点
|
|
326
324
|
const minLength = Math.min(flatOld.length, flatNew.length);
|
|
327
|
-
const domIndex = { value: 0 }; //
|
|
325
|
+
const domIndex = { value: 0 }; // 搜索旧节点的索引
|
|
326
|
+
const insertionIndex = { value: 0 }; // 逻辑插入位置的索引
|
|
328
327
|
// 跟踪已处理的节点,用于确定正确的位置
|
|
329
328
|
const processedNodes = new Set<Node>();
|
|
330
329
|
|
|
@@ -347,86 +346,79 @@ export function updateChildren(
|
|
|
347
346
|
}
|
|
348
347
|
}
|
|
349
348
|
} else if (typeof oldChild === "string" || typeof oldChild === "number") {
|
|
350
|
-
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
node.nodeType === Node.TEXT_NODE &&
|
|
362
|
-
node.parentNode === element &&
|
|
363
|
-
node.textContent === oldText
|
|
364
|
-
) {
|
|
365
|
-
oldNode = node;
|
|
366
|
-
// 更新 domIndex 到找到的文本节点之后
|
|
367
|
-
domIndex.value = j + 1;
|
|
368
|
-
break;
|
|
349
|
+
// RFC 0048 & RFC 0053 关键修复:如果 element 是保留元素(第三方组件),跳过文本节点查找
|
|
350
|
+
// 防止框架错误地管理第三方组件内部的文本节点
|
|
351
|
+
if (shouldPreserveElement(element)) {
|
|
352
|
+
// 对于保留元素,不查找文本节点,直接设置为 null
|
|
353
|
+
oldNode = null;
|
|
354
|
+
} else {
|
|
355
|
+
oldNode = findTextNode(element, domIndex, processedNodes);
|
|
356
|
+
if (oldNode) {
|
|
357
|
+
const nodeIndex = Array.from(element.childNodes).indexOf(oldNode as ChildNode);
|
|
358
|
+
if (nodeIndex !== -1 && nodeIndex >= domIndex.value) {
|
|
359
|
+
domIndex.value = nodeIndex + 1;
|
|
369
360
|
}
|
|
370
361
|
}
|
|
362
|
+
// RFC 0048 & RFC 0053 关键修复:移除 fallback 内容匹配搜索
|
|
363
|
+
// ...
|
|
371
364
|
}
|
|
372
365
|
}
|
|
373
366
|
|
|
374
367
|
// 处理文本节点(oldChild 是字符串/数字)
|
|
375
368
|
if (typeof oldChild === "string" || typeof oldChild === "number") {
|
|
376
369
|
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
377
|
-
const oldText = String(oldChild);
|
|
378
370
|
const newText = String(newChild);
|
|
379
371
|
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
// 关键修复:如果 oldNode 为 null 但 oldText === newText,
|
|
403
|
-
// 说明 DOM 中可能已经存在一个匹配的文本节点,只是没有被 findTextNode 找到
|
|
404
|
-
// 我们需要查找并标记它,避免在清理阶段被误删
|
|
405
|
-
// 这种情况可能发生在文本节点和元素节点混合排列时,domIndex 位置不准确
|
|
406
|
-
// 从 domIndex.value 开始搜索,因为这是 findTextNode 停止的位置
|
|
407
|
-
// 这样可以更准确地找到对应的文本节点
|
|
408
|
-
for (let j = domIndex.value; j < element.childNodes.length; j++) {
|
|
409
|
-
const node = element.childNodes[j];
|
|
410
|
-
if (
|
|
411
|
-
node.nodeType === Node.TEXT_NODE &&
|
|
412
|
-
node.parentNode === element &&
|
|
413
|
-
node.textContent === newText &&
|
|
414
|
-
!processedNodes.has(node)
|
|
415
|
-
) {
|
|
416
|
-
// 找到匹配的文本节点,标记为已处理
|
|
417
|
-
processedNodes.add(node);
|
|
418
|
-
break; // 只标记第一个匹配的节点
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
372
|
+
// RFC 0048 & RFC 0053 关键修复:在调用 updateOrCreateTextNode 之前,检查 element 是否是保留元素
|
|
373
|
+
if (shouldPreserveElement(element)) {
|
|
374
|
+
// 跳过保留元素的文本节点处理
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 计算插入位置
|
|
379
|
+
const insertBeforeNode =
|
|
380
|
+
insertionIndex.value < element.childNodes.length
|
|
381
|
+
? element.childNodes[insertionIndex.value]
|
|
382
|
+
: null;
|
|
383
|
+
|
|
384
|
+
const updatedNode = updateOrCreateTextNode(
|
|
385
|
+
element,
|
|
386
|
+
oldNode,
|
|
387
|
+
newText,
|
|
388
|
+
insertBeforeNode
|
|
389
|
+
);
|
|
390
|
+
if (updatedNode) {
|
|
391
|
+
processedNodes.add(updatedNode);
|
|
392
|
+
// 无论是否复用旧节点,逻辑上我们都占据了一个位置
|
|
393
|
+
insertionIndex.value++;
|
|
422
394
|
}
|
|
423
395
|
} else {
|
|
424
396
|
// 类型变化:文本 -> 元素/Fragment
|
|
425
|
-
|
|
397
|
+
const targetNode =
|
|
398
|
+
insertionIndex.value < element.childNodes.length
|
|
399
|
+
? element.childNodes[insertionIndex.value]
|
|
400
|
+
: null;
|
|
401
|
+
|
|
426
402
|
if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
427
|
-
|
|
403
|
+
// 如果 oldNode 就在当前位置,直接替换
|
|
404
|
+
if (oldNode && oldNode === targetNode && oldNode.parentNode === element) {
|
|
405
|
+
element.replaceChild(newChild, oldNode);
|
|
406
|
+
} else {
|
|
407
|
+
// 否则在目标位置插入,然后移除旧节点(如果需要)
|
|
408
|
+
element.insertBefore(newChild, targetNode);
|
|
409
|
+
removeNodeIfNotPreserved(element, oldNode);
|
|
410
|
+
}
|
|
411
|
+
processedNodes.add(newChild);
|
|
412
|
+
insertionIndex.value++;
|
|
428
413
|
} else if (newChild instanceof DocumentFragment) {
|
|
429
|
-
|
|
414
|
+
// 关键修复:跟踪 Fragment 中的所有子节点
|
|
415
|
+
if (processedNodes) {
|
|
416
|
+
for (let i = 0; i < newChild.childNodes.length; i++) {
|
|
417
|
+
processedNodes.add(newChild.childNodes[i]);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
element.insertBefore(newChild, targetNode);
|
|
421
|
+
removeNodeIfNotPreserved(element, oldNode);
|
|
430
422
|
}
|
|
431
423
|
}
|
|
432
424
|
}
|
|
@@ -437,75 +429,56 @@ export function updateChildren(
|
|
|
437
429
|
}
|
|
438
430
|
|
|
439
431
|
if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (prevChild instanceof HTMLElement || prevChild instanceof SVGElement) {
|
|
452
|
-
if (prevChild.parentNode === element) {
|
|
453
|
-
// 找到前一个元素,newChild 应该在它之后
|
|
454
|
-
targetNextSibling = prevChild.nextSibling;
|
|
455
|
-
foundPreviousElement = true;
|
|
456
|
-
break;
|
|
457
|
-
}
|
|
432
|
+
// 计算目标插入位置 (insertBeforeNode)
|
|
433
|
+
const insertBeforeNode =
|
|
434
|
+
insertionIndex.value < element.childNodes.length
|
|
435
|
+
? element.childNodes[insertionIndex.value]
|
|
436
|
+
: null;
|
|
437
|
+
|
|
438
|
+
// 即使 newChild === oldNode,如果位置不对也需要移动
|
|
439
|
+
// 使用 insertBeforeNode 确保它在正确的位置
|
|
440
|
+
if (newChild === oldNode) {
|
|
441
|
+
if (newChild.nextSibling !== insertBeforeNode) {
|
|
442
|
+
element.insertBefore(newChild, insertBeforeNode);
|
|
458
443
|
}
|
|
444
|
+
} else {
|
|
445
|
+
// RFC 0053 关键修复:直接使用 insertBefore 确保插入到正确位置
|
|
446
|
+
// replaceOrInsertElement 会尝试基于 oldNode (此处为 insertBeforeNode) 的 nextSibling 插入,
|
|
447
|
+
// 这对于旨在基于位置插入的场景会导致 Off-By-One 错误(插入到了后面)
|
|
448
|
+
element.insertBefore(newChild, insertBeforeNode);
|
|
459
449
|
}
|
|
460
450
|
|
|
461
|
-
// 如果没有找到前一个元素,newChild 应该在开头
|
|
462
|
-
if (!foundPreviousElement) {
|
|
463
|
-
// 找到第一个非保留、未处理的子节点
|
|
464
|
-
const firstChild = Array.from(element.childNodes).find(
|
|
465
|
-
(node) => !shouldPreserveElement(node) && !processedNodes.has(node)
|
|
466
|
-
);
|
|
467
|
-
targetNextSibling = firstChild || null;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// 检查 newChild 是否已经在正确位置
|
|
471
|
-
const isInCorrectPosition =
|
|
472
|
-
newChild.parentNode === element && newChild.nextSibling === targetNextSibling;
|
|
473
|
-
|
|
474
|
-
// 如果 newChild === oldChild 且位置正确,说明是同一个元素且位置正确
|
|
475
|
-
if (newChild === oldChild && isInCorrectPosition) {
|
|
476
|
-
// 标记为已处理
|
|
477
|
-
if (oldNode) processedNodes.add(oldNode);
|
|
478
|
-
processedNodes.add(newChild);
|
|
479
|
-
continue; // 元素已经在正确位置,不需要更新
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// 如果 newChild 是从缓存复用的(与 oldChild 不同),或者位置不对,需要调整
|
|
483
|
-
// 使用 oldNode 作为参考(如果存在),但目标位置基于数组顺序
|
|
484
|
-
const referenceNode = oldNode && oldNode.parentNode === element ? oldNode : null;
|
|
485
|
-
replaceOrInsertElementAtPosition(
|
|
486
|
-
element,
|
|
487
|
-
newChild,
|
|
488
|
-
referenceNode,
|
|
489
|
-
targetNextSibling
|
|
490
|
-
);
|
|
491
|
-
|
|
492
|
-
// 关键修复:在替换元素后,从 processedNodes 中移除旧的元素引用
|
|
493
|
-
// 这样可以防止旧的 span 元素内部的文本节点被误判为不应该移除
|
|
494
|
-
if (oldNode && oldNode !== newChild) {
|
|
495
|
-
processedNodes.delete(oldNode);
|
|
496
|
-
}
|
|
497
451
|
// 标记新元素为已处理
|
|
498
452
|
processedNodes.add(newChild);
|
|
453
|
+
insertionIndex.value++;
|
|
499
454
|
} else {
|
|
500
455
|
// 类型变化:元素 -> 文本/Fragment
|
|
501
|
-
|
|
456
|
+
const targetNode =
|
|
457
|
+
insertionIndex.value < element.childNodes.length
|
|
458
|
+
? element.childNodes[insertionIndex.value]
|
|
459
|
+
: null;
|
|
460
|
+
|
|
502
461
|
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
503
462
|
const newTextNode = document.createTextNode(String(newChild));
|
|
504
|
-
|
|
505
|
-
//
|
|
463
|
+
(newTextNode as any).__wsxManaged = true; // 标记为框架管理
|
|
464
|
+
// 优先替换或插入
|
|
465
|
+
if (oldNode && oldNode === targetNode && oldNode.parentNode === element) {
|
|
466
|
+
element.replaceChild(newTextNode, oldNode);
|
|
467
|
+
} else {
|
|
468
|
+
element.insertBefore(newTextNode, targetNode);
|
|
469
|
+
removeNodeIfNotPreserved(element, oldNode);
|
|
470
|
+
}
|
|
506
471
|
processedNodes.add(newTextNode);
|
|
472
|
+
insertionIndex.value++;
|
|
507
473
|
} else if (newChild instanceof DocumentFragment) {
|
|
508
|
-
|
|
474
|
+
// 关键修复:跟踪 Fragment 中的所有子节点,防止被误删
|
|
475
|
+
if (processedNodes) {
|
|
476
|
+
for (let i = 0; i < newChild.childNodes.length; i++) {
|
|
477
|
+
processedNodes.add(newChild.childNodes[i]);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
element.insertBefore(newChild, targetNode);
|
|
481
|
+
removeNodeIfNotPreserved(element, oldNode);
|
|
509
482
|
}
|
|
510
483
|
}
|
|
511
484
|
}
|
|
@@ -15,6 +15,14 @@ export function collectPreservedElements(element: HTMLElement | SVGElement): Nod
|
|
|
15
15
|
const preserved: Node[] = [];
|
|
16
16
|
for (let i = 0; i < element.childNodes.length; i++) {
|
|
17
17
|
const child = element.childNodes[i];
|
|
18
|
+
|
|
19
|
+
// RFC 0048 & RFC 0053 关键修复:文本节点通常不应该被 collectPreservedElements 捕获
|
|
20
|
+
// 因为它们会通过 updateChildren 的主循环进行 reconcile
|
|
21
|
+
// 只有当父元素本身是保留元素时,文本节点才应该被保留(但在 updateChildren 中,这种情况会跳过处理)
|
|
22
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
if (shouldPreserveElement(child)) {
|
|
19
27
|
preserved.push(child);
|
|
20
28
|
}
|
|
@@ -77,20 +85,20 @@ export function findElementNode(
|
|
|
77
85
|
*/
|
|
78
86
|
export function findTextNode(
|
|
79
87
|
parent: HTMLElement | SVGElement,
|
|
80
|
-
domIndex: { value: number }
|
|
88
|
+
domIndex: { value: number },
|
|
89
|
+
processedNodes: Set<Node>
|
|
81
90
|
): Node | null {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
// RFC 0053: 从当前索引开始查找第一个未被处理的、框架管理的文本节点
|
|
92
|
+
for (let i = domIndex.value; i < parent.childNodes.length; i++) {
|
|
93
|
+
const node = parent.childNodes[i];
|
|
94
|
+
if (
|
|
95
|
+
node.nodeType === Node.TEXT_NODE &&
|
|
96
|
+
(node as any).__wsxManaged === true &&
|
|
97
|
+
!processedNodes.has(node)
|
|
98
|
+
) {
|
|
99
|
+
domIndex.value = i + 1;
|
|
100
|
+
return node;
|
|
90
101
|
}
|
|
91
|
-
// 跳过元素节点和其他类型的节点(它们会在自己的迭代中处理)
|
|
92
|
-
// 关键:必须递增 domIndex,否则会无限循环
|
|
93
|
-
domIndex.value++;
|
|
94
102
|
}
|
|
95
103
|
return null;
|
|
96
104
|
}
|
|
@@ -117,45 +125,52 @@ export function shouldUpdateTextNode(
|
|
|
117
125
|
export function updateOrCreateTextNode(
|
|
118
126
|
parent: HTMLElement | SVGElement,
|
|
119
127
|
oldNode: Node | null,
|
|
120
|
-
newText: string
|
|
128
|
+
newText: string,
|
|
129
|
+
insertBeforeNode?: Node | null
|
|
121
130
|
): Node {
|
|
131
|
+
// RFC 0048 & RFC 0053 关键修复:如果 parent 是保留元素(第三方组件),跳过处理
|
|
132
|
+
// 防止框架错误地管理第三方组件内部的文本节点
|
|
133
|
+
if (shouldPreserveElement(parent)) {
|
|
134
|
+
// 如果 parent 是保留元素,直接返回 oldNode(如果存在)或创建新节点但不插入
|
|
135
|
+
// 注意:这种情况下,文本节点应该由第三方组件自己管理
|
|
136
|
+
if (oldNode && oldNode.nodeType === Node.TEXT_NODE) {
|
|
137
|
+
return oldNode;
|
|
138
|
+
}
|
|
139
|
+
// 对于保留元素,我们不应该插入文本节点,但为了兼容性,返回一个虚拟节点
|
|
140
|
+
// 实际上,这种情况不应该发生,因为保留元素的子节点不应该被框架处理
|
|
141
|
+
return document.createTextNode(newText);
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
if (oldNode && oldNode.nodeType === Node.TEXT_NODE) {
|
|
123
145
|
// 只有当文本内容不同时才更新
|
|
124
146
|
if (oldNode.textContent !== newText) {
|
|
125
147
|
oldNode.textContent = newText;
|
|
126
148
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
}
|
|
149
|
+
// RFC 0053 扩展:确保节点在正确的位置
|
|
150
|
+
// 如果指定了插入位置且当前节点不在该位置,则移动它
|
|
151
|
+
if (insertBeforeNode !== undefined) {
|
|
152
|
+
if (oldNode !== insertBeforeNode && oldNode.nextSibling !== insertBeforeNode) {
|
|
153
|
+
parent.insertBefore(oldNode, insertBeforeNode);
|
|
145
154
|
}
|
|
146
155
|
}
|
|
156
|
+
return oldNode;
|
|
157
|
+
} else {
|
|
158
|
+
// RFC 0048 & RFC 0053 关键修复:文本节点应该基于位置匹配,而不是内容匹配
|
|
159
|
+
// 当 oldNode 为 null 时,直接创建新节点,不进行内容匹配搜索
|
|
160
|
+
// 这防止了日历等场景中相同内容在不同位置被错误匹配的问题
|
|
161
|
+
// 例如:3 月的 "30" 在底部行,4 月的第一个日期是 "1",不应该将 "30" 错误匹配到 "1" 的位置
|
|
162
|
+
// 如果 oldNode 为 null,说明在当前位置没有找到对应的文本节点,应该创建新节点
|
|
147
163
|
|
|
148
|
-
// 如果没有找到相同内容的文本节点,创建新节点
|
|
149
164
|
const newTextNode = document.createTextNode(newText);
|
|
165
|
+
(newTextNode as any).__wsxManaged = true; // 标记为框架管理
|
|
150
166
|
if (oldNode && !shouldPreserveElement(oldNode)) {
|
|
151
167
|
parent.replaceChild(newTextNode, oldNode);
|
|
152
168
|
} else {
|
|
153
|
-
parent.insertBefore(newTextNode,
|
|
169
|
+
parent.insertBefore(newTextNode, insertBeforeNode ?? null);
|
|
154
170
|
}
|
|
155
171
|
return newTextNode;
|
|
156
172
|
}
|
|
157
173
|
}
|
|
158
|
-
|
|
159
174
|
/**
|
|
160
175
|
* 移除节点(如果不应该保留)
|
|
161
176
|
*/
|
|
@@ -281,6 +296,7 @@ export function appendNewChild(
|
|
|
281
296
|
|
|
282
297
|
if (typeof child === "string" || typeof child === "number") {
|
|
283
298
|
const newTextNode = document.createTextNode(String(child));
|
|
299
|
+
(newTextNode as any).__wsxManaged = true; // 标记为框架管理
|
|
284
300
|
parent.appendChild(newTextNode);
|
|
285
301
|
// 关键修复:标记新创建的文本节点为已处理,防止在移除阶段被误删
|
|
286
302
|
if (processedNodes) {
|
|
@@ -305,6 +321,13 @@ export function appendNewChild(
|
|
|
305
321
|
processedNodes.add(child);
|
|
306
322
|
}
|
|
307
323
|
} else if (child instanceof DocumentFragment) {
|
|
324
|
+
// 关键修复:记录 Fragment 中的所有子节点为已处理
|
|
325
|
+
// 否则它们会被后续的 collectNodesToRemove 误删
|
|
326
|
+
if (processedNodes) {
|
|
327
|
+
for (let i = 0; i < child.childNodes.length; i++) {
|
|
328
|
+
processedNodes.add(child.childNodes[i]);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
308
331
|
parent.appendChild(child);
|
|
309
332
|
}
|
|
310
333
|
}
|
|
@@ -347,32 +370,49 @@ export function shouldRemoveNode(
|
|
|
347
370
|
cacheKeyMap: Map<string, HTMLElement | SVGElement>,
|
|
348
371
|
processedNodes?: Set<Node>
|
|
349
372
|
): boolean {
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
373
|
+
// RFC 0048 & RFC 0053 关键修复:文本节点应该由框架管理,不应该被 shouldPreserveElement 保留
|
|
374
|
+
// 文本节点本身不是元素,shouldPreserveElement 对文本节点总是返回 true
|
|
375
|
+
// 但是,如果文本节点的父元素是由框架管理的,文本节点也应该由框架管理
|
|
376
|
+
// 只有当文本节点的父元素是保留元素时,文本节点才应该被保留
|
|
377
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
378
|
+
// 如果是框架管理的文本节点
|
|
379
|
+
if ((node as any).__wsxManaged === true) {
|
|
380
|
+
// 如果已被处理过,保留
|
|
381
|
+
if (processedNodes && processedNodes.has(node)) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
// 否则移除
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 如果是非框架管理的文本节点(第三方注入)
|
|
389
|
+
// 检查其父元素是否被保留
|
|
390
|
+
const parent = node.parentNode;
|
|
391
|
+
if (parent && (parent instanceof HTMLElement || parent instanceof SVGElement)) {
|
|
392
|
+
if (shouldPreserveElement(parent)) {
|
|
393
|
+
// 如果父元素是保留元素,文本节点也应该被保留
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// 对于未标记且父元素未保留的文本,默认移除以防止泄漏
|
|
398
|
+
return true;
|
|
399
|
+
} else {
|
|
400
|
+
// 对于元素节点,使用原有的逻辑
|
|
401
|
+
if (shouldPreserveElement(node)) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
353
404
|
}
|
|
354
405
|
|
|
355
|
-
|
|
356
|
-
|
|
406
|
+
const isProcessed = processedNodes && processedNodes.has(node);
|
|
407
|
+
|
|
408
|
+
if (shouldPreserveElement(node)) {
|
|
357
409
|
return false;
|
|
358
410
|
}
|
|
359
411
|
|
|
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
|
-
}
|
|
412
|
+
if (node.nodeType === Node.TEXT_NODE && isProcessed) {
|
|
413
|
+
return false;
|
|
373
414
|
}
|
|
374
415
|
|
|
375
|
-
// 检查是否在新子元素集合中(通过引用)
|
|
376
416
|
if (
|
|
377
417
|
node instanceof HTMLElement ||
|
|
378
418
|
node instanceof SVGElement ||
|
|
@@ -382,7 +422,6 @@ export function shouldRemoveNode(
|
|
|
382
422
|
return false;
|
|
383
423
|
}
|
|
384
424
|
|
|
385
|
-
// 检查是否通过 cache key 匹配
|
|
386
425
|
if (node instanceof HTMLElement || node instanceof SVGElement) {
|
|
387
426
|
const cacheKey = getElementCacheKey(node);
|
|
388
427
|
if (cacheKey && cacheKeyMap.has(cacheKey)) {
|
|
@@ -446,7 +485,8 @@ export function collectNodesToRemove(
|
|
|
446
485
|
|
|
447
486
|
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
448
487
|
const node = parent.childNodes[i];
|
|
449
|
-
|
|
488
|
+
const removed = shouldRemoveNode(node, elementSet, cacheKeyMap, processedNodes);
|
|
489
|
+
if (removed) {
|
|
450
490
|
nodesToRemove.push(node);
|
|
451
491
|
}
|
|
452
492
|
}
|
package/src/web-component.ts
CHANGED
|
@@ -225,9 +225,10 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
225
225
|
this.shadowRoot.appendChild(content);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
|
|
228
|
+
// 移除旧内容(保留样式元素、未标记元素和新渲染的内容)
|
|
229
|
+
// 关键修复 (RFC-0042): 使用 childNodes 而不是 children,确保文本节点也被清理
|
|
230
|
+
// 否则,在 Shadow DOM 根部的文本节点会发生泄漏
|
|
231
|
+
const oldNodes = Array.from(this.shadowRoot.childNodes).filter((child) => {
|
|
231
232
|
// 保留新添加的内容(或已经在 shadowRoot 中的 content)
|
|
232
233
|
if (child === content) {
|
|
233
234
|
return false;
|
|
@@ -237,13 +238,13 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
237
238
|
return false;
|
|
238
239
|
}
|
|
239
240
|
// 保留未标记的元素(第三方库注入的元素、自定义元素)
|
|
240
|
-
//
|
|
241
|
+
// 对于文本节点,shouldPreserveElement 逻辑已在 element-marking.ts 中优化
|
|
241
242
|
if (shouldPreserveElement(child)) {
|
|
242
243
|
return false;
|
|
243
244
|
}
|
|
244
245
|
return true;
|
|
245
246
|
});
|
|
246
|
-
|
|
247
|
+
oldNodes.forEach((node) => node.remove());
|
|
247
248
|
|
|
248
249
|
// 7. 恢复 adopted stylesheets(在 DOM 操作之后,确保样式不被意外移除)
|
|
249
250
|
// 关键修复:在 DOM 操作之后恢复样式,防止样式在 DOM 操作过程中被意外清空
|