@wsxjs/wsx-core 0.0.23 → 0.0.24

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.
@@ -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
- cacheManager?: DOMCacheManager
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
- // 关键修复:如果 findTextNode 返回 null,尝试从当前 domIndex 位置开始查找文本节点
288
- // 这可以处理文本节点存在但 domIndex 不正确的情况
289
- // Bug 1 修复:从 domIndex.value 开始搜索,而不是从 0 开始,避免重新处理已处理的节点
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
- if (node.nodeType === Node.TEXT_NODE) {
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
- updateOrCreateTextNode(element, oldNode, newText);
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
- // 关键修复:即使 newChild === oldChild,如果它是元素,h() 应该已经调用了 updateElement 来更新其子元素
340
- // 但是,为了确保子元素确实被更新了,我们不应该跳过,而是让后续代码确保元素在正确位置
341
- // 如果 h() 已经正确更新了子元素,那么这里只需要确保元素在正确位置即可
342
- if (
343
- newChild === oldChild &&
344
- (newChild instanceof HTMLElement || newChild instanceof SVGElement)
345
- ) {
346
- // 同一个元素引用,h() 应该已经通过 updateElement 更新了其子元素
347
- // 但是,如果 cacheManager 可用,我们可以验证并确保子元素确实被更新了
348
- if (cacheManager) {
349
- const childMetadata = cacheManager.getMetadata(newChild);
350
- if (childMetadata) {
351
- // 如果元数据存在,说明 h() 已经更新了子元素
352
- // 只需要确保元素在正确位置
353
- if (oldNode === newChild && newChild.parentNode === element) {
354
- // 元素已经在正确位置,且 h() 应该已经更新了其子元素
355
- // 不需要额外处理
356
- continue;
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
- if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
369
- // 如果 newChild 已经在 DOM 中且位置正确,不需要替换
370
- if (newChild.parentNode === element && oldNode === newChild) {
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
- replaceOrInsertElement(element, newChild, oldNode);
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
- removeNodes(element, nodesToRemove);
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
- if (node.nodeType === Node.TEXT_NODE) {
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
- ): void {
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
- // Bug 2 修复:如果 oldNode 为 null,说明 findTextNode 没有找到对应的文本节点
126
- // 此时不应该盲目更新第一个找到的文本节点,而应该创建新节点
127
- // 因为:
128
- // 1. 如果文本内容相同,调用方已经跳过了更新(在 element-update.ts 中)
129
- // 2. 如果文本内容不同,应该创建新节点,而不是更新错误的节点
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
- if (oldNode && !shouldPreserveElement(oldNode)) {
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 if (newChild.parentNode !== parent) {
169
- parent.insertBefore(newChild, oldNode || null);
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(parent: HTMLElement | SVGElement, child: JSXChildren): void {
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
- parent.appendChild(document.createTextNode(String(child)));
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
- if (child.parentNode !== parent) {
189
- parent.appendChild(child);
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(parent: HTMLElement | SVGElement, nodes: Node[]): void {
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
  }