@wsxjs/wsx-core 0.0.22 → 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.
@@ -6,10 +6,26 @@
6
6
  */
7
7
 
8
8
  import { shouldUseSVGNamespace, getSVGAttributeName } from "./svg-utils";
9
- import { flattenChildren, type JSXChildren } from "./dom-utils";
9
+ import { type JSXChildren } from "./dom-utils";
10
10
  import { setSmartProperty, isFrameworkInternalProp } from "./props-utils";
11
- import { shouldPreserveElement, getElementCacheKey } from "./element-marking";
11
+ import { shouldPreserveElement } from "./element-marking";
12
12
  import type { DOMCacheManager } from "../dom-cache-manager";
13
+ import {
14
+ collectPreservedElements,
15
+ findElementNode,
16
+ findTextNode,
17
+ updateOrCreateTextNode,
18
+ removeNodeIfNotPreserved,
19
+ replaceOrInsertElement,
20
+ replaceOrInsertElementAtPosition,
21
+ appendNewChild,
22
+ buildNewChildrenMaps,
23
+ deduplicateCacheKeys,
24
+ collectNodesToRemove,
25
+ removeNodes,
26
+ reinsertPreservedElements,
27
+ flattenChildrenSafe,
28
+ } from "./update-children-helpers";
13
29
 
14
30
  /**
15
31
  * Removes a property from an element.
@@ -24,7 +40,16 @@ function removeProp(
24
40
 
25
41
  // 处理特殊属性
26
42
  if (key === "ref") {
27
- // ref 是回调,不需要移除
43
+ // 关键修复:当 ref 被移除时,调用回调并传入 null
44
+ // 这确保组件可以清理引用,避免使用已移除的元素
45
+ // 例如:LanguageSwitcher 的 dropdownElement 应该在元素被移除时设置为 null
46
+ if (typeof oldValue === "function") {
47
+ try {
48
+ oldValue(null);
49
+ } catch {
50
+ // 忽略回调错误
51
+ }
52
+ }
28
53
  return;
29
54
  }
30
55
 
@@ -43,8 +68,17 @@ function removeProp(
43
68
  }
44
69
 
45
70
  if (key.startsWith("on") && typeof oldValue === "function") {
46
- // 事件监听器:需要移除(但无法获取原始监听器,所以跳过)
47
- // 注意:这可能导致内存泄漏,但在实际使用中,事件监听器通常不会变化
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
+ }
48
82
  return;
49
83
  }
50
84
 
@@ -119,9 +153,21 @@ function applySingleProp(
119
153
  // 处理事件监听器
120
154
  if (key.startsWith("on") && typeof value === "function") {
121
155
  const eventName = key.slice(2).toLowerCase();
122
- // 注意:这里会重复添加事件监听器,但这是预期的行为
123
- // 在实际使用中,事件监听器通常不会频繁变化
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
+ // 添加新监听器并保存引用
124
168
  element.addEventListener(eventName, value as EventListener);
169
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
+ (element as any)[listenerKey] = value;
125
171
  return;
126
172
  }
127
173
 
@@ -236,208 +282,177 @@ export function updateProps(
236
282
  export function updateChildren(
237
283
  element: HTMLElement | SVGElement,
238
284
  oldChildren: JSXChildren[],
239
- newChildren: JSXChildren[]
285
+ newChildren: JSXChildren[],
286
+ _cacheManager?: DOMCacheManager // 可选参数,保留以保持 API 兼容性
240
287
  ): void {
241
- const flatOld = flattenChildren(oldChildren);
242
- const flatNew = flattenChildren(newChildren);
288
+ const flatOld = flattenChildrenSafe(oldChildren);
289
+ const flatNew = flattenChildrenSafe(newChildren);
243
290
 
244
- // 阶段 4 简化版:只处理相同数量的子节点
245
- const minLength = Math.min(flatOld.length, flatNew.length);
291
+ // 收集需要保留的元素(第三方库注入的元素)
292
+ const preservedElements = collectPreservedElements(element);
246
293
 
247
294
  // 更新现有子节点
248
- // 关键:直接使用 oldChild 作为 oldNode(如果它是元素),因为它已经在 DOM 中
249
- // 对于文本节点,按顺序匹配(跳过应该保留的元素节点)
250
- let domIndex = 0; // DOM 中的实际索引,用于匹配文本节点
295
+ const minLength = Math.min(flatOld.length, flatNew.length);
296
+ const domIndex = { value: 0 }; // 使用对象包装,使其可在函数间传递
297
+ // 跟踪已处理的节点,用于确定正确的位置
298
+ const processedNodes = new Set<Node>();
299
+
251
300
  for (let i = 0; i < minLength; i++) {
252
301
  const oldChild = flatOld[i];
253
302
  const newChild = flatNew[i];
254
303
 
255
- // 找到与 oldChild 对应的实际 DOM 节点
256
- // 关键:oldChild 是上次渲染的元素引用,如果它在 DOM 中,直接使用它
304
+ // 查找对应的 DOM 节点
257
305
  let oldNode: Node | null = null;
258
306
  if (oldChild instanceof HTMLElement || oldChild instanceof SVGElement) {
259
- // 元素节点:检查 oldChild 是否在 DOM 中
260
- // 如果 oldChild 的 parentNode 是当前 element,说明它在 DOM 中
261
- if (oldChild.parentNode === element) {
262
- // 关键修复:确保 oldChild 不是应该保留的元素(手动创建的元素、第三方库注入的元素)
263
- // 如果 oldChild 是应该保留的元素,不应该在"更新现有子节点"循环中处理它
264
- if (!shouldPreserveElement(oldChild)) {
265
- oldNode = oldChild;
266
- }
267
- } else {
268
- // oldChild 不在 DOM 中,尝试通过 cache key 找到对应的 DOM 节点
269
- // 这可以处理元素被替换但 cache key 相同的情况
270
- const oldCacheKey = getElementCacheKey(oldChild);
271
- if (oldCacheKey) {
272
- // 遍历 DOM 中的子节点,找到具有相同 cache key 的节点
273
- for (let j = 0; j < element.childNodes.length; j++) {
274
- const domChild = element.childNodes[j];
275
- if (domChild instanceof HTMLElement || domChild instanceof SVGElement) {
276
- // 跳过应该保留的元素(手动创建的元素、第三方库注入的元素)
277
- if (shouldPreserveElement(domChild)) {
278
- continue;
279
- }
280
- const domCacheKey = getElementCacheKey(domChild);
281
- if (domCacheKey === oldCacheKey) {
282
- oldNode = domChild;
283
- break;
284
- }
285
- }
286
- }
307
+ oldNode = findElementNode(oldChild, element);
308
+ // 关键修复:当处理元素节点时,需要更新 domIndex 以跳过该元素
309
+ // 这样,下一个文本节点的查找位置才是正确的
310
+ if (oldNode && oldNode.parentNode === element) {
311
+ // 找到 oldNode 在 DOM 中的位置
312
+ const nodeIndex = Array.from(element.childNodes).indexOf(oldNode as ChildNode);
313
+ if (nodeIndex !== -1 && nodeIndex >= domIndex.value) {
314
+ // 更新 domIndex 到 oldNode 之后的位置
315
+ domIndex.value = nodeIndex + 1;
287
316
  }
288
317
  }
289
- // 如果 oldChild 不在 DOM 中且找不到对应的节点,oldNode 保持为 null
290
318
  } else if (typeof oldChild === "string" || typeof oldChild === "number") {
291
- // 文本节点:按顺序找到对应的文本节点(跳过所有元素节点)
292
- // 关键:跳过应该保留的元素节点(手动创建的元素、第三方库注入的元素)
293
- while (domIndex < element.childNodes.length) {
294
- const node = element.childNodes[domIndex];
295
- if (node.nodeType === Node.TEXT_NODE) {
296
- oldNode = node;
297
- domIndex++;
298
- break;
299
- } else if (node.nodeType === Node.ELEMENT_NODE) {
300
- // 跳过所有元素节点(不管是保留的还是框架管理的)
301
- // 因为元素节点会在自己的迭代中处理
302
- // 注意:手动创建的元素会被 shouldPreserveElement 保护,不会被移除
303
- domIndex++;
304
- } else {
305
- // 跳过其他类型的节点
306
- domIndex++;
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;
338
+ }
307
339
  }
308
340
  }
309
341
  }
310
342
 
311
- // 如果是文本节点,更新文本内容
343
+ // 处理文本节点(oldChild 是字符串/数字)
312
344
  if (typeof oldChild === "string" || typeof oldChild === "number") {
313
345
  if (typeof newChild === "string" || typeof newChild === "number") {
314
346
  const oldText = String(oldChild);
315
347
  const newText = String(newChild);
316
348
 
317
- // 关键修复:始终检查 DOM 中的实际文本内容,而不仅仅依赖 oldChild
318
- // 这样可以确保即使元数据不同步,也能正确更新
349
+ // Bug 2 修复:只有当文本内容确实需要更新时才调用 updateOrCreateTextNode
350
+ // 如果 oldText === newText 且 oldNode 为 null,说明文本节点可能已经存在且内容正确
351
+ // 或者不需要创建,因此不应该调用 updateOrCreateTextNode
319
352
  const needsUpdate =
320
353
  oldText !== newText ||
321
354
  (oldNode &&
322
355
  oldNode.nodeType === Node.TEXT_NODE &&
323
356
  oldNode.textContent !== newText);
324
357
 
325
- if (!needsUpdate) {
326
- // 文本内容确实相同,跳过更新
327
- continue;
328
- }
329
-
330
- if (oldNode && oldNode.nodeType === Node.TEXT_NODE) {
331
- // 更新现有文本节点
332
- oldNode.textContent = newText;
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
+ }
333
366
  } else {
334
- // 创建新的文本节点
335
- const newTextNode = document.createTextNode(newText);
336
- if (oldNode && !shouldPreserveElement(oldNode)) {
337
- element.replaceChild(newTextNode, oldNode);
338
- } else {
339
- element.insertBefore(newTextNode, oldNode || null);
367
+ // 即使不需要更新,也要标记为已处理(文本节点已存在且内容正确)
368
+ if (oldNode && oldNode.parentNode === element) {
369
+ processedNodes.add(oldNode);
340
370
  }
341
371
  }
342
372
  } else {
343
- // 类型变化:文本 -> 元素
344
- if (oldNode && !shouldPreserveElement(oldNode)) {
345
- element.removeChild(oldNode);
346
- }
373
+ // 类型变化:文本 -> 元素/Fragment
374
+ removeNodeIfNotPreserved(element, oldNode);
347
375
  if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
348
- if (newChild.parentNode !== element) {
349
- element.insertBefore(newChild, oldNode || null);
350
- }
376
+ replaceOrInsertElement(element, newChild, oldNode);
351
377
  } else if (newChild instanceof DocumentFragment) {
352
378
  element.insertBefore(newChild, oldNode || null);
353
379
  }
354
380
  }
355
- } else if (oldChild instanceof HTMLElement || oldChild instanceof SVGElement) {
356
- // 关键修复:如果 oldNode 是应该保留的元素(手动创建的元素、第三方库注入的元素),跳过处理
357
- // 这些元素不在 oldChildren newChildren 中,应该在第二步被保留
381
+ }
382
+ // 处理元素节点(oldChild 是元素)
383
+ else if (oldChild instanceof HTMLElement || oldChild instanceof SVGElement) {
358
384
  if (oldNode && shouldPreserveElement(oldNode)) {
359
- // 跳过应该保留的元素,继续处理下一个
360
- continue;
385
+ continue; // 跳过保留的元素
361
386
  }
362
387
 
363
- // 如果是元素节点,检查是否是同一个元素
364
- if (newChild === oldChild) {
365
- // 同一个元素,不需要更新(元素内容会在 updateElement 中更新)
366
- continue;
367
- } else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
368
- // 不同的元素引用,需要替换
369
- // 检查 cache key:如果 cache key 相同,说明是同一个逻辑位置,应该替换
370
- const oldCacheKey =
371
- oldNode && (oldNode instanceof HTMLElement || oldNode instanceof SVGElement)
372
- ? getElementCacheKey(oldNode)
373
- : null;
374
- const newCacheKey = getElementCacheKey(newChild);
375
- const hasSameCacheKey = oldCacheKey && newCacheKey && oldCacheKey === newCacheKey;
376
-
377
- if (oldNode) {
378
- // oldNode 存在(oldChild 在 DOM 中)
379
- if (!shouldPreserveElement(oldNode)) {
380
- // 可以替换
381
- if (oldNode !== newChild) {
382
- // 如果 newChild 已经在 DOM 中,需要先移除它(避免重复)
383
- if (newChild.parentNode === element) {
384
- // newChild 已经在当前 element 中
385
- // 如果 cache key 相同,说明是同一个逻辑位置,应该替换 oldNode
386
- if (hasSameCacheKey) {
387
- // 如果 newChild 就是 oldNode,不需要替换
388
- if (newChild !== oldNode) {
389
- // 替换旧元素(replaceChild 会自动从 newChild 的旧位置移除它)
390
- // 但是,如果 newChild 在 oldNode 之后,replaceChild 会先移除 newChild,然后替换 oldNode
391
- // 这会导致 newChild 移动到 oldNode 的位置,这是正确的
392
- // 如果 newChild 在 oldNode 之前,replaceChild 也会将 newChild 移动到 oldNode 的位置
393
- // 所以,无论 newChild 在哪里,replaceChild 都会将其移动到 oldNode 的位置
394
- element.replaceChild(newChild, oldNode);
395
- }
396
- } else {
397
- // cache key 不同,说明 newChild 在错误的位置
398
- // 先移除 newChild(它可能在错误的位置)
399
- element.removeChild(newChild);
400
- // 然后替换 oldNode
401
- element.replaceChild(newChild, oldNode);
402
- }
403
- } else if (newChild.parentNode) {
404
- // newChild 在其他父元素中,先移除
405
- newChild.parentNode.removeChild(newChild);
406
- // 然后替换 oldNode
407
- element.replaceChild(newChild, oldNode);
408
- } else {
409
- // newChild 不在 DOM 中,直接替换
410
- element.replaceChild(newChild, oldNode);
411
- }
412
- }
413
- } else {
414
- // 应该保留旧节点,只添加新节点(不替换)
415
- if (newChild.parentNode !== element) {
416
- // 如果 newChild 在其他父元素中,先移除
417
- if (newChild.parentNode) {
418
- newChild.parentNode.removeChild(newChild);
419
- }
420
- element.insertBefore(newChild, oldNode.nextSibling);
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;
421
406
  }
422
407
  }
423
- } else {
424
- // oldNode 不存在(oldChild 不在 DOM 中),直接添加新元素
425
- if (newChild.parentNode !== element) {
426
- // 如果 newChild 在其他父元素中,先移除
427
- if (newChild.parentNode) {
428
- newChild.parentNode.removeChild(newChild);
429
- }
430
- element.appendChild(newChild);
431
- }
432
408
  }
433
- } else {
434
- // 类型变化:元素 -> 文本
435
- if (oldNode && !shouldPreserveElement(oldNode)) {
436
- element.removeChild(oldNode);
409
+
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; // 元素已经在正确位置,不需要更新
437
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
+ // 标记新元素为已处理
447
+ processedNodes.add(newChild);
448
+ } else {
449
+ // 类型变化:元素 -> 文本/Fragment
450
+ removeNodeIfNotPreserved(element, oldNode);
438
451
  if (typeof newChild === "string" || typeof newChild === "number") {
439
452
  const newTextNode = document.createTextNode(String(newChild));
440
453
  element.insertBefore(newTextNode, oldNode?.nextSibling || null);
454
+ // 关键修复:标记新创建的文本节点为已处理,防止在移除阶段被误删
455
+ processedNodes.add(newTextNode);
441
456
  } else if (newChild instanceof DocumentFragment) {
442
457
  element.insertBefore(newChild, oldNode?.nextSibling || null);
443
458
  }
@@ -447,158 +462,26 @@ export function updateChildren(
447
462
 
448
463
  // 添加新子节点
449
464
  for (let i = minLength; i < flatNew.length; i++) {
450
- const newChild = flatNew[i];
451
- if (newChild === null || newChild === undefined || newChild === false) {
452
- continue;
453
- }
454
-
455
- if (typeof newChild === "string" || typeof newChild === "number") {
456
- element.appendChild(document.createTextNode(String(newChild)));
457
- } else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
458
- // 确保子元素正确添加到当前父容器
459
- // 如果 newChild 已经在 DOM 中,需要检查它是否在正确的位置
460
- if (newChild.parentNode === element) {
461
- // newChild 已经在当前 element 中
462
- // 检查它是否在正确的位置(应该在最后,因为这是"添加新子节点"部分)
463
- const currentIndex = Array.from(element.childNodes).indexOf(newChild);
464
- const expectedIndex = element.childNodes.length - 1;
465
- if (currentIndex !== expectedIndex) {
466
- // 位置不对,移动到正确位置(末尾)
467
- element.removeChild(newChild);
468
- element.appendChild(newChild);
469
- }
470
- // 如果位置正确,跳过(避免重复添加)
471
- // 注意:元素内容会在 updateElement 中更新,所以这里只需要确保位置正确
472
- continue;
473
- } else if (newChild.parentNode) {
474
- // newChild 在其他父元素中,先移除
475
- newChild.parentNode.removeChild(newChild);
476
- }
477
- // 添加 newChild 到当前 element 的末尾
478
- element.appendChild(newChild);
479
- } else if (newChild instanceof DocumentFragment) {
480
- element.appendChild(newChild);
481
- }
465
+ appendNewChild(element, flatNew[i], processedNodes);
482
466
  }
483
467
 
484
- // 移除多余子节点(阶段 5:正确处理元素保留)
485
- // 关键:需要跳过"应该保留"的元素(第三方库注入的元素)
486
- // 以及已经在 newChildren 中的元素(通过元素引用或 cache key 匹配)
487
- const nodesToRemove: Node[] = [];
488
- const newChildSet = new Set<HTMLElement | SVGElement | DocumentFragment>();
489
- const newChildCacheKeyMap = new Map<string, HTMLElement | SVGElement>();
490
- // 只将元素节点添加到 Set 中(文本节点不能直接比较)
491
- for (const child of flatNew) {
492
- if (
493
- child instanceof HTMLElement ||
494
- child instanceof SVGElement ||
495
- child instanceof DocumentFragment
496
- ) {
497
- newChildSet.add(child);
498
- // 同时收集 cache key 到 Map,用于匹配(即使元素引用不同,cache key 相同也认为是同一个元素)
499
- // 注意:DocumentFragment 没有 cache key,只能通过引用匹配
500
- if (child instanceof HTMLElement || child instanceof SVGElement) {
501
- const cacheKey = getElementCacheKey(child);
502
- if (cacheKey) {
503
- // 如果 cache key 已存在,保留最新的元素(newChild)
504
- newChildCacheKeyMap.set(cacheKey, child);
505
- }
506
- }
507
- }
508
- }
468
+ // 移除多余子节点(使用纯函数简化逻辑)
469
+ // 步骤 1: 构建新子元素的引用集合和 cache key 映射
470
+ const { elementSet, cacheKeyMap } = buildNewChildrenMaps(flatNew);
509
471
 
510
- // 第一步:处理重复的 cache key(如果 DOM 中有多个元素具有相同的 cache key,只保留 newChild)
511
- // 注意:需要从后往前遍历,避免在循环中修改 DOM 导致索引问题
512
- // 关键:这个步骤在"更新现有子节点"循环之后执行,所以需要处理那些在"更新现有子节点"循环中没有处理的重复元素
513
- const processedCacheKeys = new Set<string>();
514
- // 构建 newChild 到其在 flatNew 中索引的映射,用于确定正确位置
515
- const newChildToIndexMap = new Map<HTMLElement | SVGElement, number>();
516
- for (let i = 0; i < flatNew.length; i++) {
517
- const child = flatNew[i];
518
- if (child instanceof HTMLElement || child instanceof SVGElement) {
519
- newChildToIndexMap.set(child, i);
520
- }
521
- }
472
+ // 步骤 2: 处理重复的 cache key(确保每个 cache key 在 DOM 中只出现一次)
473
+ deduplicateCacheKeys(element, cacheKeyMap);
522
474
 
523
- for (let i = element.childNodes.length - 1; i >= 0; i--) {
524
- const child = element.childNodes[i];
525
- if (child instanceof HTMLElement || child instanceof SVGElement) {
526
- // 关键修复:跳过应该保留的元素(第三方库注入的元素)
527
- if (shouldPreserveElement(child)) {
528
- continue;
529
- }
530
- const cacheKey = getElementCacheKey(child);
531
- if (
532
- cacheKey &&
533
- newChildCacheKeyMap.has(cacheKey) &&
534
- !processedCacheKeys.has(cacheKey)
535
- ) {
536
- processedCacheKeys.add(cacheKey);
537
- const newChild = newChildCacheKeyMap.get(cacheKey)!;
538
- // 如果 child 不是 newChild,说明是旧元素,应该被移除或替换
539
- if (child !== newChild) {
540
- // 如果 newChild 已经在 DOM 中,移除旧元素
541
- if (newChild.parentNode === element) {
542
- // newChild 已经在 DOM 中,移除旧元素
543
- // 注意:newChild 已经在 DOM 中,所以不需要再次添加
544
- // 但需要确保 newChild 在正确的位置(通过 replaceChild 移动到旧元素的位置)
545
- // 这样可以确保 newChild 在正确的位置,而不是在错误的位置
546
- // 但是,如果 newChild 在 child 之后,replaceChild 会先移除 newChild,然后替换 child
547
- // 这会导致 newChild 移动到 child 的位置,这是正确的
548
- // 如果 newChild 在 child 之前,replaceChild 也会将 newChild 移动到 child 的位置
549
- // 所以,无论 newChild 在哪里,replaceChild 都会将其移动到 child 的位置
550
- element.replaceChild(newChild, child);
551
- } else {
552
- // newChild 不在 DOM 中,替换旧元素
553
- element.replaceChild(newChild, child);
554
- }
555
- } else {
556
- // child === newChild,说明是同一个元素,不需要处理
557
- // 但需要确保它在正确的位置(这应该在"更新现有子节点"循环中处理)
558
- }
559
- }
560
- }
561
- }
562
-
563
- // 第二步:移除不在 newChildren 中的元素
564
- for (let i = 0; i < element.childNodes.length; i++) {
565
- const child = element.childNodes[i];
475
+ // 步骤 3: 收集需要移除的节点(跳过保留元素和新子元素)
476
+ const nodesToRemove = collectNodesToRemove(element, elementSet, cacheKeyMap, processedNodes);
566
477
 
567
- // 跳过应该保留的元素(第三方库注入的元素)
568
- if (shouldPreserveElement(child)) {
569
- continue;
570
- }
478
+ // 步骤 4: 批量移除节点(从后往前,避免索引变化)
479
+ // 传递 cacheManager 以便在移除元素时调用 ref 回调
480
+ removeNodes(element, nodesToRemove, _cacheManager);
571
481
 
572
- // 跳过已经在 newChildren 中的元素(通过元素引用或 cache key 匹配)
573
- if (child instanceof HTMLElement || child instanceof SVGElement) {
574
- // 方法 1: 直接元素引用匹配
575
- if (newChildSet.has(child)) {
576
- continue;
577
- }
578
- // 方法 2: cache key 匹配(用于处理语言切换等场景,元素引用可能不同但 cache key 相同)
579
- const cacheKey = getElementCacheKey(child);
580
- if (cacheKey && newChildCacheKeyMap.has(cacheKey)) {
581
- // 已经在第一步处理过了,跳过
582
- continue;
583
- }
584
- } else if (child instanceof DocumentFragment) {
585
- // DocumentFragment 只能通过引用匹配
586
- if (newChildSet.has(child)) {
587
- continue;
588
- }
589
- }
590
-
591
- // 只有不应该保留且不在 newChildren 中的节点才添加到移除列表
592
- nodesToRemove.push(child);
593
- }
594
-
595
- // 统一移除(从后往前移除,避免索引变化)
596
- for (let i = nodesToRemove.length - 1; i >= 0; i--) {
597
- const node = nodesToRemove[i];
598
- if (node.parentNode === element) {
599
- element.removeChild(node);
600
- }
601
- }
482
+ // 步骤 5: 重新插入所有保留的元素到 DOM 末尾
483
+ // 这确保了第三方库注入的元素不会丢失
484
+ reinsertPreservedElements(element, preservedElements);
602
485
  }
603
486
 
604
487
  /**
@@ -629,5 +512,5 @@ export function updateElement(
629
512
  updateProps(element, oldProps, newProps, tag);
630
513
 
631
514
  // 更新 children
632
- updateChildren(element, oldChildren, newChildren);
515
+ updateChildren(element, oldChildren, newChildren, cacheManager);
633
516
  }