@wsxjs/wsx-core 0.0.26 → 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.
@@ -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,
@@ -124,7 +122,9 @@ function applySingleProp(
124
122
  tag: string,
125
123
  isSVG: boolean
126
124
  ): void {
127
- if (value === null || value === undefined || value === false) {
125
+ // 关键修复:不要在这里提前返回 false,因为布尔属性 false 需要特殊处理(移除属性)
126
+ // 只对 null 和 undefined 提前返回
127
+ if (value === null || value === undefined) {
128
128
  return;
129
129
  }
130
130
 
@@ -175,6 +175,35 @@ function applySingleProp(
175
175
  if (typeof value === "boolean") {
176
176
  if (value) {
177
177
  element.setAttribute(key, "");
178
+ // 对于 input 元素,同时设置 JavaScript 属性
179
+ if (element instanceof HTMLInputElement) {
180
+ if (key === "checked") {
181
+ element.checked = true;
182
+ } else if (key === "disabled") {
183
+ element.disabled = true;
184
+ } else if (key === "readonly") {
185
+ element.readOnly = true; // 注意:JavaScript 属性是 readOnly(驼峰)
186
+ }
187
+ } else if (element instanceof HTMLOptionElement && key === "selected") {
188
+ element.selected = true;
189
+ }
190
+ } else {
191
+ // 关键修复:当布尔属性为 false 时,应该移除属性
192
+ // 这样可以确保元素状态正确更新(例如:radio button 取消选择时移除 checked 属性)
193
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
194
+ element.removeAttribute(attributeName);
195
+ // 对于 input 元素,同时设置 JavaScript 属性为 false
196
+ if (element instanceof HTMLInputElement) {
197
+ if (key === "checked") {
198
+ element.checked = false;
199
+ } else if (key === "disabled") {
200
+ element.disabled = false;
201
+ } else if (key === "readonly") {
202
+ element.readOnly = false; // 注意:JavaScript 属性是 readOnly(驼峰)
203
+ }
204
+ } else if (element instanceof HTMLOptionElement && key === "selected") {
205
+ element.selected = false;
206
+ }
178
207
  }
179
208
  return;
180
209
  }
@@ -293,7 +322,8 @@ export function updateChildren(
293
322
 
294
323
  // 更新现有子节点
295
324
  const minLength = Math.min(flatOld.length, flatNew.length);
296
- const domIndex = { value: 0 }; // 使用对象包装,使其可在函数间传递
325
+ const domIndex = { value: 0 }; // 搜索旧节点的索引
326
+ const insertionIndex = { value: 0 }; // 逻辑插入位置的索引
297
327
  // 跟踪已处理的节点,用于确定正确的位置
298
328
  const processedNodes = new Set<Node>();
299
329
 
@@ -316,66 +346,79 @@ export function updateChildren(
316
346
  }
317
347
  }
318
348
  } else if (typeof oldChild === "string" || typeof oldChild === "number") {
319
- oldNode = findTextNode(element, domIndex);
320
- // RFC-0044 修复:fallback 搜索必须验证文本内容
321
- // 在缓存元素复用场景下,不能盲目返回第一个找到的文本节点
322
- // 必须确保内容匹配,否则会导致错误的更新
323
- if (!oldNode && element.childNodes.length > 0) {
324
- const oldText = String(oldChild);
325
- for (let j = domIndex.value; j < element.childNodes.length; j++) {
326
- const node = element.childNodes[j];
327
- // 关键修复:只检查直接子文本节点,确保 node.parentNode === element
328
- // 并且必须验证文本内容是否匹配
329
- if (
330
- node.nodeType === Node.TEXT_NODE &&
331
- node.parentNode === element &&
332
- node.textContent === oldText
333
- ) {
334
- oldNode = node;
335
- // 更新 domIndex 到找到的文本节点之后
336
- domIndex.value = j + 1;
337
- 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;
338
360
  }
339
361
  }
362
+ // RFC 0048 & RFC 0053 关键修复:移除 fallback 内容匹配搜索
363
+ // ...
340
364
  }
341
365
  }
342
366
 
343
367
  // 处理文本节点(oldChild 是字符串/数字)
344
368
  if (typeof oldChild === "string" || typeof oldChild === "number") {
345
369
  if (typeof newChild === "string" || typeof newChild === "number") {
346
- const oldText = String(oldChild);
347
370
  const newText = String(newChild);
348
371
 
349
- // Bug 2 修复:只有当文本内容确实需要更新时才调用 updateOrCreateTextNode
350
- // 如果 oldText === newText 且 oldNode 为 null,说明文本节点可能已经存在且内容正确
351
- // 或者不需要创建,因此不应该调用 updateOrCreateTextNode
352
- const needsUpdate =
353
- oldText !== newText ||
354
- (oldNode &&
355
- oldNode.nodeType === Node.TEXT_NODE &&
356
- oldNode.textContent !== newText);
357
-
358
- if (needsUpdate) {
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
- }
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++;
371
394
  }
372
395
  } else {
373
396
  // 类型变化:文本 -> 元素/Fragment
374
- removeNodeIfNotPreserved(element, oldNode);
397
+ const targetNode =
398
+ insertionIndex.value < element.childNodes.length
399
+ ? element.childNodes[insertionIndex.value]
400
+ : null;
401
+
375
402
  if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
376
- replaceOrInsertElement(element, newChild, oldNode);
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++;
377
413
  } else if (newChild instanceof DocumentFragment) {
378
- element.insertBefore(newChild, oldNode || null);
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);
379
422
  }
380
423
  }
381
424
  }
@@ -386,75 +429,56 @@ export function updateChildren(
386
429
  }
387
430
 
388
431
  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;
406
- }
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);
407
443
  }
444
+ } else {
445
+ // RFC 0053 关键修复:直接使用 insertBefore 确保插入到正确位置
446
+ // replaceOrInsertElement 会尝试基于 oldNode (此处为 insertBeforeNode) 的 nextSibling 插入,
447
+ // 这对于旨在基于位置插入的场景会导致 Off-By-One 错误(插入到了后面)
448
+ element.insertBefore(newChild, insertBeforeNode);
408
449
  }
409
450
 
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);
428
- continue; // 元素已经在正确位置,不需要更新
429
- }
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
451
  // 标记新元素为已处理
447
452
  processedNodes.add(newChild);
453
+ insertionIndex.value++;
448
454
  } else {
449
455
  // 类型变化:元素 -> 文本/Fragment
450
- removeNodeIfNotPreserved(element, oldNode);
456
+ const targetNode =
457
+ insertionIndex.value < element.childNodes.length
458
+ ? element.childNodes[insertionIndex.value]
459
+ : null;
460
+
451
461
  if (typeof newChild === "string" || typeof newChild === "number") {
452
462
  const newTextNode = document.createTextNode(String(newChild));
453
- element.insertBefore(newTextNode, oldNode?.nextSibling || null);
454
- // 关键修复:标记新创建的文本节点为已处理,防止在移除阶段被误删
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
+ }
455
471
  processedNodes.add(newTextNode);
472
+ insertionIndex.value++;
456
473
  } else if (newChild instanceof DocumentFragment) {
457
- element.insertBefore(newChild, oldNode?.nextSibling || null);
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);
458
482
  }
459
483
  }
460
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
- while (domIndex.value < parent.childNodes.length) {
83
- const node = parent.childNodes[domIndex.value];
84
- // 关键修复:只检查直接子文本节点,确保 node.parentNode === parent
85
- // 这样可以防止将元素内部的文本节点(如 span 内部的文本节点)误判为独立的文本节点
86
- if (node.nodeType === Node.TEXT_NODE && node.parentNode === parent) {
87
- const textNode = node;
88
- domIndex.value++;
89
- return textNode;
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
- return oldNode;
128
- } else {
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
- }
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, oldNode || null);
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
- if (shouldPreserveElement(node)) {
352
- return false;
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
- if (node.nodeType === Node.TEXT_NODE && processedNodes && processedNodes.has(node)) {
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
- if (shouldRemoveNode(node, elementSet, cacheKeyMap, processedNodes)) {
488
+ const removed = shouldRemoveNode(node, elementSet, cacheKeyMap, processedNodes);
489
+ if (removed) {
450
490
  nodesToRemove.push(node);
451
491
  }
452
492
  }
@@ -225,9 +225,10 @@ export abstract class WebComponent extends BaseComponent {
225
225
  this.shadowRoot.appendChild(content);
226
226
  }
227
227
 
228
- // 移除旧内容(保留样式元素和未标记元素)
229
- // 关键修复:使用 shouldPreserveElement() 来保护第三方库注入的元素
230
- const oldChildren = Array.from(this.shadowRoot.children).filter((child) => {
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
- // 这是 RFC 0037 Phase 5 的核心:保护未标记元素
241
+ // 对于文本节点,shouldPreserveElement 逻辑已在 element-marking.ts 中优化
241
242
  if (shouldPreserveElement(child)) {
242
243
  return false;
243
244
  }
244
245
  return true;
245
246
  });
246
- oldChildren.forEach((child) => child.remove());
247
+ oldNodes.forEach((node) => node.remove());
247
248
 
248
249
  // 7. 恢复 adopted stylesheets(在 DOM 操作之后,确保样式不被意外移除)
249
250
  // 关键修复:在 DOM 操作之后恢复样式,防止样式在 DOM 操作过程中被意外清空