@wsxjs/wsx-core 0.0.27 → 0.0.30

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,
@@ -25,6 +23,7 @@ import {
25
23
  removeNodes,
26
24
  reinsertPreservedElements,
27
25
  flattenChildrenSafe,
26
+ replaceOrInsertElementAtPosition,
28
27
  } from "./update-children-helpers";
29
28
 
30
29
  /**
@@ -324,7 +323,8 @@ export function updateChildren(
324
323
 
325
324
  // 更新现有子节点
326
325
  const minLength = Math.min(flatOld.length, flatNew.length);
327
- const domIndex = { value: 0 }; // 使用对象包装,使其可在函数间传递
326
+ const domIndex = { value: 0 }; // 搜索旧节点的索引
327
+ const insertionIndex = { value: 0 }; // 逻辑插入位置的索引
328
328
  // 跟踪已处理的节点,用于确定正确的位置
329
329
  const processedNodes = new Set<Node>();
330
330
 
@@ -347,86 +347,79 @@ export function updateChildren(
347
347
  }
348
348
  }
349
349
  } else if (typeof oldChild === "string" || typeof oldChild === "number") {
350
- oldNode = findTextNode(element, domIndex);
351
- // RFC-0044 修复:fallback 搜索必须验证文本内容
352
- // 在缓存元素复用场景下,不能盲目返回第一个找到的文本节点
353
- // 必须确保内容匹配,否则会导致错误的更新
354
- if (!oldNode && element.childNodes.length > 0) {
355
- const oldText = String(oldChild);
356
- for (let j = domIndex.value; j < element.childNodes.length; j++) {
357
- const node = element.childNodes[j];
358
- // 关键修复:只检查直接子文本节点,确保 node.parentNode === element
359
- // 并且必须验证文本内容是否匹配
360
- if (
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;
350
+ // RFC 0048 & RFC 0053 关键修复:如果 element 是保留元素(第三方组件),跳过文本节点查找
351
+ // 防止框架错误地管理第三方组件内部的文本节点
352
+ if (shouldPreserveElement(element)) {
353
+ // 对于保留元素,不查找文本节点,直接设置为 null
354
+ oldNode = null;
355
+ } else {
356
+ oldNode = findTextNode(element, domIndex, processedNodes);
357
+ if (oldNode) {
358
+ const nodeIndex = Array.from(element.childNodes).indexOf(oldNode as ChildNode);
359
+ if (nodeIndex !== -1 && nodeIndex >= domIndex.value) {
360
+ domIndex.value = nodeIndex + 1;
369
361
  }
370
362
  }
363
+ // RFC 0048 & RFC 0053 关键修复:移除 fallback 内容匹配搜索
364
+ // ...
371
365
  }
372
366
  }
373
367
 
374
368
  // 处理文本节点(oldChild 是字符串/数字)
375
369
  if (typeof oldChild === "string" || typeof oldChild === "number") {
376
370
  if (typeof newChild === "string" || typeof newChild === "number") {
377
- const oldText = String(oldChild);
378
371
  const newText = String(newChild);
379
372
 
380
- // Bug 2 修复:只有当文本内容确实需要更新时才调用 updateOrCreateTextNode
381
- // 如果 oldText === newText 且 oldNode 为 null,说明文本节点可能已经存在且内容正确
382
- // 或者不需要创建,因此不应该调用 updateOrCreateTextNode
383
- const needsUpdate =
384
- oldText !== newText ||
385
- (oldNode &&
386
- oldNode.nodeType === Node.TEXT_NODE &&
387
- oldNode.textContent !== newText);
388
-
389
- if (needsUpdate) {
390
- // RFC-0044 修复:使用返回的节点引用直接标记
391
- // updateOrCreateTextNode 现在返回更新/创建的节点
392
- // 这样可以准确标记,避免搜索失败导致的重复创建
393
- const updatedNode = updateOrCreateTextNode(element, oldNode, newText);
394
- if (updatedNode && !processedNodes.has(updatedNode)) {
395
- processedNodes.add(updatedNode);
396
- }
397
- } else {
398
- // 即使不需要更新,也要标记为已处理(文本节点已存在且内容正确)
399
- if (oldNode && oldNode.parentNode === element) {
400
- processedNodes.add(oldNode);
401
- } else if (!oldNode && oldText === newText) {
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
- }
373
+ // RFC 0048 & RFC 0053 关键修复:在调用 updateOrCreateTextNode 之前,检查 element 是否是保留元素
374
+ if (shouldPreserveElement(element)) {
375
+ // 跳过保留元素的文本节点处理
376
+ continue;
377
+ }
378
+
379
+ // 计算插入位置
380
+ const insertBeforeNode =
381
+ insertionIndex.value < element.childNodes.length
382
+ ? element.childNodes[insertionIndex.value]
383
+ : null;
384
+
385
+ const updatedNode = updateOrCreateTextNode(
386
+ element,
387
+ oldNode,
388
+ newText,
389
+ insertBeforeNode
390
+ );
391
+ if (updatedNode) {
392
+ processedNodes.add(updatedNode);
393
+ // 无论是否复用旧节点,逻辑上我们都占据了一个位置
394
+ insertionIndex.value++;
422
395
  }
423
396
  } else {
424
397
  // 类型变化:文本 -> 元素/Fragment
425
- removeNodeIfNotPreserved(element, oldNode);
398
+ const targetNode =
399
+ insertionIndex.value < element.childNodes.length
400
+ ? element.childNodes[insertionIndex.value]
401
+ : null;
402
+
426
403
  if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
427
- replaceOrInsertElement(element, newChild, oldNode);
404
+ // 如果 oldNode 就在当前位置,直接替换
405
+ if (oldNode && oldNode === targetNode && oldNode.parentNode === element) {
406
+ element.replaceChild(newChild, oldNode);
407
+ } else {
408
+ // 否则在目标位置插入,然后移除旧节点(如果需要)
409
+ element.insertBefore(newChild, targetNode);
410
+ removeNodeIfNotPreserved(element, oldNode);
411
+ }
412
+ processedNodes.add(newChild);
413
+ insertionIndex.value++;
428
414
  } else if (newChild instanceof DocumentFragment) {
429
- element.insertBefore(newChild, oldNode || null);
415
+ // 关键修复:跟踪 Fragment 中的所有子节点
416
+ if (processedNodes) {
417
+ for (let i = 0; i < newChild.childNodes.length; i++) {
418
+ processedNodes.add(newChild.childNodes[i]);
419
+ }
420
+ }
421
+ element.insertBefore(newChild, targetNode);
422
+ removeNodeIfNotPreserved(element, oldNode);
430
423
  }
431
424
  }
432
425
  }
@@ -437,75 +430,51 @@ export function updateChildren(
437
430
  }
438
431
 
439
432
  if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
440
- // 关键修复:确定正确的位置
441
- // 策略:基于 flatNew 数组的顺序来确定位置,而不是 DOM 中的实际顺序
442
- // 因为某些元素可能被隐藏(position: absolute),导致 DOM 顺序与数组顺序不同
443
-
444
- // 找到索引 i 之前的所有元素,确定 newChild 应该在这些元素之后
445
- let targetNextSibling: Node | null = null;
446
- let foundPreviousElement = false;
447
-
448
- // 从后往前查找,找到最后一个在 DOM 中的前一个元素
449
- for (let j = i - 1; j >= 0; j--) {
450
- const prevChild = flatNew[j];
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
- }
458
- }
459
- }
460
-
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;
433
+ // 计算目标插入位置 (insertBeforeNode)
434
+ const insertBeforeNode =
435
+ insertionIndex.value < element.childNodes.length
436
+ ? element.childNodes[insertionIndex.value]
437
+ : null;
438
+
439
+ // 甚至 newChild === oldNode,如果位置不对也需要移动
440
+ // 使用 helper 处理元素替换和插入,支持 HTML 解析内容的自动等价性匹配
485
441
  replaceOrInsertElementAtPosition(
486
442
  element,
487
- newChild,
488
- referenceNode,
489
- targetNextSibling
443
+ newChild as HTMLElement | SVGElement,
444
+ oldNode,
445
+ insertBeforeNode,
446
+ processedNodes
490
447
  );
491
448
 
492
- // 关键修复:在替换元素后,从 processedNodes 中移除旧的元素引用
493
- // 这样可以防止旧的 span 元素内部的文本节点被误判为不应该移除
494
- if (oldNode && oldNode !== newChild) {
495
- processedNodes.delete(oldNode);
496
- }
497
- // 标记新元素为已处理
498
- processedNodes.add(newChild);
449
+ insertionIndex.value++;
499
450
  } else {
500
451
  // 类型变化:元素 -> 文本/Fragment
501
- removeNodeIfNotPreserved(element, oldNode);
452
+ const targetNode =
453
+ insertionIndex.value < element.childNodes.length
454
+ ? element.childNodes[insertionIndex.value]
455
+ : null;
456
+
502
457
  if (typeof newChild === "string" || typeof newChild === "number") {
503
458
  const newTextNode = document.createTextNode(String(newChild));
504
- element.insertBefore(newTextNode, oldNode?.nextSibling || null);
505
- // 关键修复:标记新创建的文本节点为已处理,防止在移除阶段被误删
459
+ (newTextNode as any).__wsxManaged = true; // 标记为框架管理
460
+ // 优先替换或插入
461
+ if (oldNode && oldNode === targetNode && oldNode.parentNode === element) {
462
+ element.replaceChild(newTextNode, oldNode);
463
+ } else {
464
+ element.insertBefore(newTextNode, targetNode);
465
+ removeNodeIfNotPreserved(element, oldNode);
466
+ }
506
467
  processedNodes.add(newTextNode);
468
+ insertionIndex.value++;
507
469
  } else if (newChild instanceof DocumentFragment) {
508
- element.insertBefore(newChild, oldNode?.nextSibling || null);
470
+ // 关键修复:跟踪 Fragment 中的所有子节点,防止被误删
471
+ if (processedNodes) {
472
+ for (let i = 0; i < newChild.childNodes.length; i++) {
473
+ processedNodes.add(newChild.childNodes[i]);
474
+ }
475
+ }
476
+ element.insertBefore(newChild, targetNode);
477
+ removeNodeIfNotPreserved(element, oldNode);
509
478
  }
510
479
  }
511
480
  }
@@ -565,3 +534,85 @@ export function updateElement(
565
534
  // 更新 children
566
535
  updateChildren(element, oldChildren, newChildren, cacheManager);
567
536
  }
537
+
538
+ /**
539
+ * Recursively reconciles an old element to match a new element's structure.
540
+ * This is a pure function that updates the oldParent in-place to match newParent.
541
+ *
542
+ * Used by LightComponent to update DOM without full replacement, preserving:
543
+ * - Element references (important for event listeners, focus state)
544
+ * - User-added JSX children
545
+ * - Third-party library injected elements
546
+ *
547
+ * @param oldParent - The existing DOM element to update
548
+ * @param newParent - The new element structure to match
549
+ */
550
+ export function reconcileElement(oldParent: HTMLElement, newParent: HTMLElement): void {
551
+ const oldChildren = Array.from(oldParent.childNodes);
552
+ const newChildren = Array.from(newParent.childNodes);
553
+
554
+ const maxLength = Math.max(oldChildren.length, newChildren.length);
555
+
556
+ for (let i = 0; i < maxLength; i++) {
557
+ const oldChild = oldChildren[i];
558
+ const newChild = newChildren[i];
559
+
560
+ if (!newChild) {
561
+ // 新的子节点不存在,删除旧的
562
+ oldChild?.remove();
563
+ } else if (!oldChild) {
564
+ // 旧的子节点不存在,添加新的
565
+ oldParent.appendChild(newChild.cloneNode(true));
566
+ } else if (oldChild.nodeType !== newChild.nodeType) {
567
+ // 节点类型不同,替换
568
+ oldParent.replaceChild(newChild.cloneNode(true), oldChild);
569
+ } else if (oldChild.nodeType === Node.TEXT_NODE) {
570
+ // 文本节点,更新内容
571
+ if (oldChild.textContent !== newChild.textContent) {
572
+ oldChild.textContent = newChild.textContent;
573
+ }
574
+ } else if (oldChild.nodeType === Node.ELEMENT_NODE) {
575
+ const oldEl = oldChild as HTMLElement;
576
+ const newEl = newChild as HTMLElement;
577
+
578
+ // 元素节点
579
+ if (oldEl.tagName !== newEl.tagName) {
580
+ // 标签不同,替换
581
+ oldParent.replaceChild(newEl.cloneNode(true), oldEl);
582
+ } else {
583
+ // 标签相同,更新属性
584
+ // 1. 移除旧属性
585
+ Array.from(oldEl.attributes).forEach((attr) => {
586
+ if (!newEl.hasAttribute(attr.name)) {
587
+ oldEl.removeAttribute(attr.name);
588
+ }
589
+ });
590
+
591
+ // 2. 设置/更新新属性
592
+ Array.from(newEl.attributes).forEach((attr) => {
593
+ if (oldEl.getAttribute(attr.name) !== attr.value) {
594
+ oldEl.setAttribute(attr.name, attr.value);
595
+ }
596
+ });
597
+
598
+ // 3. 特殊处理:className 是 property,不是 attribute
599
+ if (oldEl.className !== newEl.className) {
600
+ oldEl.className = newEl.className;
601
+ }
602
+
603
+ // 4. 特殊处理:对于 input 元素,更新 checked 和 value 属性
604
+ if (oldEl instanceof HTMLInputElement && newEl instanceof HTMLInputElement) {
605
+ if (oldEl.checked !== newEl.checked) {
606
+ oldEl.checked = newEl.checked;
607
+ }
608
+ if (oldEl.value !== newEl.value) {
609
+ oldEl.value = newEl.value;
610
+ }
611
+ }
612
+
613
+ // 5. 递归更新子元素
614
+ reconcileElement(oldEl, newEl);
615
+ }
616
+ }
617
+ }
618
+ }