@wsxjs/wsx-core 0.0.23 → 0.0.25

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/jsx.js CHANGED
@@ -100,6 +100,7 @@ var _RenderContext = class _RenderContext {
100
100
  * @param fn The function to execute (usually the render method).
101
101
  */
102
102
  static runInContext(component, fn) {
103
+ resetCounterForNewRenderCycle(component);
103
104
  const prev = _RenderContext.current;
104
105
  _RenderContext.current = component;
105
106
  try {
@@ -125,21 +126,20 @@ _RenderContext.current = null;
125
126
  var RenderContext = _RenderContext;
126
127
 
127
128
  // src/utils/cache-key.ts
128
- var POSITION_ID_KEY = "__wsxPositionId";
129
129
  var INDEX_KEY = "__wsxIndex";
130
130
  var componentElementCounters = /* @__PURE__ */ new WeakMap();
131
131
  var componentIdCache = /* @__PURE__ */ new WeakMap();
132
132
  function generateCacheKey(tag, props, componentId, component) {
133
- const positionId = props?.[POSITION_ID_KEY];
134
133
  const userKey = props?.key;
135
134
  const index = props?.[INDEX_KEY];
135
+ const positionId = props?.__wsxPositionId;
136
136
  if (userKey !== void 0 && userKey !== null) {
137
137
  return `${componentId}:${tag}:key-${String(userKey)}`;
138
138
  }
139
139
  if (index !== void 0 && index !== null) {
140
140
  return `${componentId}:${tag}:idx-${String(index)}`;
141
141
  }
142
- if (positionId !== void 0 && positionId !== null && positionId !== "no-id") {
142
+ if (positionId !== void 0 && positionId !== null) {
143
143
  return `${componentId}:${tag}:${String(positionId)}`;
144
144
  }
145
145
  if (component) {
@@ -150,6 +150,9 @@ function generateCacheKey(tag, props, componentId, component) {
150
150
  }
151
151
  return `${componentId}:${tag}:fallback-${Date.now()}-${Math.random()}`;
152
152
  }
153
+ function resetCounterForNewRenderCycle(component) {
154
+ componentElementCounters.set(component, 0);
155
+ }
153
156
  function getComponentId() {
154
157
  const component = RenderContext.getCurrentComponent();
155
158
  if (component) {
@@ -567,7 +570,9 @@ function applySingleProp(element, key, value, tag, isSVG) {
567
570
  }
568
571
  if (key.startsWith("on") && typeof value === "function") {
569
572
  const eventName = key.slice(2).toLowerCase();
573
+ const listenerKey = `__wsxListener_${eventName}`;
570
574
  element.addEventListener(eventName, value);
575
+ element[listenerKey] = value;
571
576
  return;
572
577
  }
573
578
  if (typeof value === "boolean") {
@@ -662,7 +667,7 @@ function findElementNode(oldChild, parent) {
662
667
  function findTextNode(parent, domIndex) {
663
668
  while (domIndex.value < parent.childNodes.length) {
664
669
  const node = parent.childNodes[domIndex.value];
665
- if (node.nodeType === Node.TEXT_NODE) {
670
+ if (node.nodeType === Node.TEXT_NODE && node.parentNode === parent) {
666
671
  const textNode = node;
667
672
  domIndex.value++;
668
673
  return textNode;
@@ -676,13 +681,23 @@ function updateOrCreateTextNode(parent, oldNode, newText) {
676
681
  if (oldNode.textContent !== newText) {
677
682
  oldNode.textContent = newText;
678
683
  }
684
+ return oldNode;
679
685
  } else {
686
+ if (!oldNode) {
687
+ for (let i = 0; i < parent.childNodes.length; i++) {
688
+ const node = parent.childNodes[i];
689
+ if (node.nodeType === Node.TEXT_NODE && node.parentNode === parent && node.textContent === newText) {
690
+ return node;
691
+ }
692
+ }
693
+ }
680
694
  const newTextNode = document.createTextNode(newText);
681
695
  if (oldNode && !shouldPreserveElement(oldNode)) {
682
696
  parent.replaceChild(newTextNode, oldNode);
683
697
  } else {
684
698
  parent.insertBefore(newTextNode, oldNode || null);
685
699
  }
700
+ return newTextNode;
686
701
  }
687
702
  }
688
703
  function removeNodeIfNotPreserved(parent, node) {
@@ -691,29 +706,66 @@ function removeNodeIfNotPreserved(parent, node) {
691
706
  }
692
707
  }
693
708
  function replaceOrInsertElement(parent, newChild, oldNode) {
709
+ const targetNextSibling = oldNode && shouldPreserveElement(oldNode) ? oldNode : oldNode?.nextSibling || null;
710
+ replaceOrInsertElementAtPosition(parent, newChild, oldNode, targetNextSibling);
711
+ }
712
+ function replaceOrInsertElementAtPosition(parent, newChild, oldNode, targetNextSibling) {
694
713
  if (newChild.parentNode && newChild.parentNode !== parent) {
695
714
  newChild.parentNode.removeChild(newChild);
696
715
  }
697
- if (oldNode && !shouldPreserveElement(oldNode)) {
716
+ const isInCorrectPosition = newChild.parentNode === parent && newChild.nextSibling === targetNextSibling;
717
+ if (isInCorrectPosition) {
718
+ return;
719
+ }
720
+ if (newChild.parentNode === parent) {
721
+ parent.insertBefore(newChild, targetNextSibling);
722
+ return;
723
+ }
724
+ if (oldNode && oldNode.parentNode === parent && !shouldPreserveElement(oldNode)) {
698
725
  if (oldNode !== newChild) {
699
726
  parent.replaceChild(newChild, oldNode);
700
727
  }
701
- } else if (newChild.parentNode !== parent) {
702
- parent.insertBefore(newChild, oldNode || null);
728
+ } else {
729
+ if (newChild.parentNode === parent) {
730
+ return;
731
+ }
732
+ const newChildCacheKey = getElementCacheKey(newChild);
733
+ if (!newChildCacheKey) {
734
+ const newChildContent = newChild.textContent || "";
735
+ const newChildTag = newChild.tagName.toLowerCase();
736
+ for (let i = 0; i < parent.childNodes.length; i++) {
737
+ const existingNode = parent.childNodes[i];
738
+ if (existingNode instanceof HTMLElement || existingNode instanceof SVGElement) {
739
+ const existingCacheKey = getElementCacheKey(existingNode);
740
+ if (!existingCacheKey && existingNode.tagName.toLowerCase() === newChildTag && existingNode.textContent === newChildContent && existingNode !== newChild) {
741
+ return;
742
+ }
743
+ }
744
+ }
745
+ }
746
+ parent.insertBefore(newChild, targetNextSibling);
703
747
  }
704
748
  }
705
- function appendNewChild(parent, child) {
749
+ function appendNewChild(parent, child, processedNodes) {
706
750
  if (child === null || child === void 0 || child === false) {
707
751
  return;
708
752
  }
709
753
  if (typeof child === "string" || typeof child === "number") {
710
- parent.appendChild(document.createTextNode(String(child)));
754
+ const newTextNode = document.createTextNode(String(child));
755
+ parent.appendChild(newTextNode);
756
+ if (processedNodes) {
757
+ processedNodes.add(newTextNode);
758
+ }
711
759
  } else if (child instanceof HTMLElement || child instanceof SVGElement) {
760
+ if (child.parentNode === parent) {
761
+ return;
762
+ }
712
763
  if (child.parentNode && child.parentNode !== parent) {
713
764
  child.parentNode.removeChild(child);
714
765
  }
715
- if (child.parentNode !== parent) {
716
- parent.appendChild(child);
766
+ parent.appendChild(child);
767
+ if (processedNodes) {
768
+ processedNodes.add(child);
717
769
  }
718
770
  } else if (child instanceof DocumentFragment) {
719
771
  parent.appendChild(child);
@@ -735,10 +787,22 @@ function buildNewChildrenMaps(flatNew) {
735
787
  }
736
788
  return { elementSet, cacheKeyMap };
737
789
  }
738
- function shouldRemoveNode(node, elementSet, cacheKeyMap) {
790
+ function shouldRemoveNode(node, elementSet, cacheKeyMap, processedNodes) {
739
791
  if (shouldPreserveElement(node)) {
740
792
  return false;
741
793
  }
794
+ if (node.nodeType === Node.TEXT_NODE && processedNodes && processedNodes.has(node)) {
795
+ return false;
796
+ }
797
+ if (node.nodeType === Node.TEXT_NODE && processedNodes) {
798
+ let parent = node.parentNode;
799
+ while (parent) {
800
+ if (processedNodes.has(parent) && parent.parentNode) {
801
+ return false;
802
+ }
803
+ parent = parent.parentNode;
804
+ }
805
+ }
742
806
  if (node instanceof HTMLElement || node instanceof SVGElement || node instanceof DocumentFragment) {
743
807
  if (elementSet.has(node)) {
744
808
  return false;
@@ -767,24 +831,39 @@ function deduplicateCacheKeys(parent, cacheKeyMap) {
767
831
  if (child !== newChild) {
768
832
  parent.replaceChild(newChild, child);
769
833
  }
834
+ } else if (cacheKey && cacheKeyMap.has(cacheKey) && processedCacheKeys.has(cacheKey)) {
835
+ const newChild = cacheKeyMap.get(cacheKey);
836
+ if (child !== newChild) {
837
+ parent.removeChild(child);
838
+ }
770
839
  }
771
840
  }
772
841
  }
773
842
  }
774
- function collectNodesToRemove(parent, elementSet, cacheKeyMap) {
843
+ function collectNodesToRemove(parent, elementSet, cacheKeyMap, processedNodes) {
775
844
  const nodesToRemove = [];
776
845
  for (let i = 0; i < parent.childNodes.length; i++) {
777
846
  const node = parent.childNodes[i];
778
- if (shouldRemoveNode(node, elementSet, cacheKeyMap)) {
847
+ if (shouldRemoveNode(node, elementSet, cacheKeyMap, processedNodes)) {
779
848
  nodesToRemove.push(node);
780
849
  }
781
850
  }
782
851
  return nodesToRemove;
783
852
  }
784
- function removeNodes(parent, nodes) {
853
+ function removeNodes(parent, nodes, cacheManager) {
785
854
  for (let i = nodes.length - 1; i >= 0; i--) {
786
855
  const node = nodes[i];
787
856
  if (node.parentNode === parent) {
857
+ if (cacheManager && (node instanceof HTMLElement || node instanceof SVGElement)) {
858
+ const metadata = cacheManager.getMetadata(node);
859
+ const refCallback = metadata?.ref;
860
+ if (typeof refCallback === "function") {
861
+ try {
862
+ refCallback(null);
863
+ } catch {
864
+ }
865
+ }
866
+ }
788
867
  parent.removeChild(node);
789
868
  }
790
869
  }
@@ -804,6 +883,12 @@ function flattenChildrenSafe(children) {
804
883
  function removeProp(element, key, oldValue, tag) {
805
884
  const isSVG = shouldUseSVGNamespace(tag);
806
885
  if (key === "ref") {
886
+ if (typeof oldValue === "function") {
887
+ try {
888
+ oldValue(null);
889
+ } catch {
890
+ }
891
+ }
807
892
  return;
808
893
  }
809
894
  if (key === "className" || key === "class") {
@@ -819,6 +904,13 @@ function removeProp(element, key, oldValue, tag) {
819
904
  return;
820
905
  }
821
906
  if (key.startsWith("on") && typeof oldValue === "function") {
907
+ const eventName = key.slice(2).toLowerCase();
908
+ const listenerKey = `__wsxListener_${eventName}`;
909
+ const savedListener = element[listenerKey];
910
+ if (savedListener) {
911
+ element.removeEventListener(eventName, savedListener);
912
+ delete element[listenerKey];
913
+ }
822
914
  return;
823
915
  }
824
916
  if (key === "value") {
@@ -862,7 +954,13 @@ function applySingleProp2(element, key, value, tag, isSVG) {
862
954
  }
863
955
  if (key.startsWith("on") && typeof value === "function") {
864
956
  const eventName = key.slice(2).toLowerCase();
957
+ const listenerKey = `__wsxListener_${eventName}`;
958
+ const oldListener = element[listenerKey];
959
+ if (oldListener) {
960
+ element.removeEventListener(eventName, oldListener);
961
+ }
865
962
  element.addEventListener(eventName, value);
963
+ element[listenerKey] = value;
866
964
  return;
867
965
  }
868
966
  if (typeof value === "boolean") {
@@ -917,12 +1015,13 @@ function updateProps(element, oldProps, newProps, tag) {
917
1015
  applySingleProp2(element, key, newValue, tag, isSVG);
918
1016
  }
919
1017
  }
920
- function updateChildren(element, oldChildren, newChildren, cacheManager) {
1018
+ function updateChildren(element, oldChildren, newChildren, _cacheManager) {
921
1019
  const flatOld = flattenChildrenSafe(oldChildren);
922
1020
  const flatNew = flattenChildrenSafe(newChildren);
923
1021
  const preservedElements = collectPreservedElements(element);
924
1022
  const minLength = Math.min(flatOld.length, flatNew.length);
925
1023
  const domIndex = { value: 0 };
1024
+ const processedNodes = /* @__PURE__ */ new Set();
926
1025
  for (let i = 0; i < minLength; i++) {
927
1026
  const oldChild = flatOld[i];
928
1027
  const newChild = flatNew[i];
@@ -938,9 +1037,10 @@ function updateChildren(element, oldChildren, newChildren, cacheManager) {
938
1037
  } else if (typeof oldChild === "string" || typeof oldChild === "number") {
939
1038
  oldNode = findTextNode(element, domIndex);
940
1039
  if (!oldNode && element.childNodes.length > 0) {
1040
+ const oldText = String(oldChild);
941
1041
  for (let j = domIndex.value; j < element.childNodes.length; j++) {
942
1042
  const node = element.childNodes[j];
943
- if (node.nodeType === Node.TEXT_NODE) {
1043
+ if (node.nodeType === Node.TEXT_NODE && node.parentNode === element && node.textContent === oldText) {
944
1044
  oldNode = node;
945
1045
  domIndex.value = j + 1;
946
1046
  break;
@@ -954,7 +1054,14 @@ function updateChildren(element, oldChildren, newChildren, cacheManager) {
954
1054
  const newText = String(newChild);
955
1055
  const needsUpdate = oldText !== newText || oldNode && oldNode.nodeType === Node.TEXT_NODE && oldNode.textContent !== newText;
956
1056
  if (needsUpdate) {
957
- updateOrCreateTextNode(element, oldNode, newText);
1057
+ const updatedNode = updateOrCreateTextNode(element, oldNode, newText);
1058
+ if (updatedNode && !processedNodes.has(updatedNode)) {
1059
+ processedNodes.add(updatedNode);
1060
+ }
1061
+ } else {
1062
+ if (oldNode && oldNode.parentNode === element) {
1063
+ processedNodes.add(oldNode);
1064
+ }
958
1065
  }
959
1066
  } else {
960
1067
  removeNodeIfNotPreserved(element, oldNode);
@@ -968,30 +1075,48 @@ function updateChildren(element, oldChildren, newChildren, cacheManager) {
968
1075
  if (oldNode && shouldPreserveElement(oldNode)) {
969
1076
  continue;
970
1077
  }
971
- if (newChild === oldChild && (newChild instanceof HTMLElement || newChild instanceof SVGElement)) {
972
- if (cacheManager) {
973
- const childMetadata = cacheManager.getMetadata(newChild);
974
- if (childMetadata) {
975
- if (oldNode === newChild && newChild.parentNode === element) {
976
- continue;
1078
+ if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
1079
+ let targetNextSibling = null;
1080
+ let foundPreviousElement = false;
1081
+ for (let j = i - 1; j >= 0; j--) {
1082
+ const prevChild = flatNew[j];
1083
+ if (prevChild instanceof HTMLElement || prevChild instanceof SVGElement) {
1084
+ if (prevChild.parentNode === element) {
1085
+ targetNextSibling = prevChild.nextSibling;
1086
+ foundPreviousElement = true;
1087
+ break;
977
1088
  }
978
1089
  }
979
- } else {
980
- if (oldNode === newChild && newChild.parentNode === element) {
981
- continue;
982
- }
983
1090
  }
984
- }
985
- if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
986
- if (newChild.parentNode === element && oldNode === newChild) {
1091
+ if (!foundPreviousElement) {
1092
+ const firstChild = Array.from(element.childNodes).find(
1093
+ (node) => !shouldPreserveElement(node) && !processedNodes.has(node)
1094
+ );
1095
+ targetNextSibling = firstChild || null;
1096
+ }
1097
+ const isInCorrectPosition = newChild.parentNode === element && newChild.nextSibling === targetNextSibling;
1098
+ if (newChild === oldChild && isInCorrectPosition) {
1099
+ if (oldNode) processedNodes.add(oldNode);
1100
+ processedNodes.add(newChild);
987
1101
  continue;
988
1102
  }
989
- replaceOrInsertElement(element, newChild, oldNode);
1103
+ const referenceNode = oldNode && oldNode.parentNode === element ? oldNode : null;
1104
+ replaceOrInsertElementAtPosition(
1105
+ element,
1106
+ newChild,
1107
+ referenceNode,
1108
+ targetNextSibling
1109
+ );
1110
+ if (oldNode && oldNode !== newChild) {
1111
+ processedNodes.delete(oldNode);
1112
+ }
1113
+ processedNodes.add(newChild);
990
1114
  } else {
991
1115
  removeNodeIfNotPreserved(element, oldNode);
992
1116
  if (typeof newChild === "string" || typeof newChild === "number") {
993
1117
  const newTextNode = document.createTextNode(String(newChild));
994
1118
  element.insertBefore(newTextNode, oldNode?.nextSibling || null);
1119
+ processedNodes.add(newTextNode);
995
1120
  } else if (newChild instanceof DocumentFragment) {
996
1121
  element.insertBefore(newChild, oldNode?.nextSibling || null);
997
1122
  }
@@ -999,12 +1124,12 @@ function updateChildren(element, oldChildren, newChildren, cacheManager) {
999
1124
  }
1000
1125
  }
1001
1126
  for (let i = minLength; i < flatNew.length; i++) {
1002
- appendNewChild(element, flatNew[i]);
1127
+ appendNewChild(element, flatNew[i], processedNodes);
1003
1128
  }
1004
1129
  const { elementSet, cacheKeyMap } = buildNewChildrenMaps(flatNew);
1005
1130
  deduplicateCacheKeys(element, cacheKeyMap);
1006
- const nodesToRemove = collectNodesToRemove(element, elementSet, cacheKeyMap);
1007
- removeNodes(element, nodesToRemove);
1131
+ const nodesToRemove = collectNodesToRemove(element, elementSet, cacheKeyMap, processedNodes);
1132
+ removeNodes(element, nodesToRemove, _cacheManager);
1008
1133
  reinsertPreservedElements(element, preservedElements);
1009
1134
  }
1010
1135
  function updateElement(element, newProps, newChildren, tag, cacheManager) {
package/dist/jsx.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Fragment,
3
3
  h
4
- } from "./chunk-ESZYREJK.mjs";
4
+ } from "./chunk-5Q2VEEUH.mjs";
5
5
  export {
6
6
  Fragment,
7
7
  h
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsxjs/wsx-core",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "description": "Core WSXJS - Web Components with JSX syntax",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -48,7 +48,7 @@
48
48
  "custom-elements"
49
49
  ],
50
50
  "dependencies": {
51
- "@wsxjs/wsx-logger": "0.0.23"
51
+ "@wsxjs/wsx-logger": "0.0.25"
52
52
  },
53
53
  "devDependencies": {
54
54
  "tsup": "^8.0.0",
@@ -91,6 +91,13 @@ export abstract class BaseComponent extends HTMLElement {
91
91
  */
92
92
  protected _isRendering: boolean = false;
93
93
 
94
+ /**
95
+ * 已调度渲染标志(防止在同一事件循环中重复注册 requestAnimationFrame)
96
+ * 用于批量更新:同一事件循环中的多个状态变化只触发一次渲染
97
+ * @internal
98
+ */
99
+ private _hasScheduledRender: boolean = false;
100
+
94
101
  /**
95
102
  * 子类应该重写这个方法来定义观察的属性
96
103
  * @returns 要观察的属性名数组
@@ -266,6 +273,12 @@ export abstract class BaseComponent extends HTMLElement {
266
273
  return;
267
274
  }
268
275
 
276
+ // 如果已经调度了渲染,跳过(避免在同一事件循环中重复注册 requestAnimationFrame)
277
+ // 这实现了批量更新:同一事件循环中的多个状态变化只触发一次渲染
278
+ if (this._hasScheduledRender) {
279
+ return;
280
+ }
281
+
269
282
  // 检查是否有需要持续输入的元素获得焦点(input、textarea、select、contenteditable)
270
283
  // 按钮等其他元素应该立即重渲染,以反映状态变化
271
284
  const root = this.getActiveRoot();
@@ -315,10 +328,24 @@ export abstract class BaseComponent extends HTMLElement {
315
328
  if (this._pendingRerender) {
316
329
  this._pendingRerender = false;
317
330
  }
331
+
332
+ // 标记已调度渲染(批量更新的关键)
333
+ this._hasScheduledRender = true;
334
+
318
335
  // 使用 requestAnimationFrame 而不是 queueMicrotask,确保在渲染帧中执行
319
336
  // 这样可以避免在 render() 执行期间触发的 scheduleRerender() 立即执行
320
337
  requestAnimationFrame(() => {
338
+ console.warn("[scheduleRerender] RAF callback:", {
339
+ component: this.constructor.name,
340
+ connected: this.connected,
341
+ isRendering: this._isRendering,
342
+ });
343
+
344
+ // 重置调度标志(允许后续的状态变化调度新的渲染)
345
+ this._hasScheduledRender = false;
346
+
321
347
  if (this.connected && !this._isRendering) {
348
+ console.warn("[scheduleRerender] calling _rerender()");
322
349
  // 设置渲染标志,防止在 _rerender() 执行期间再次触发
323
350
  // 注意:_isRendering 标志会在 _rerender() 的 onRendered() 调用完成后清除
324
351
  this._isRendering = true;
@@ -79,6 +79,9 @@ export abstract class LightComponent extends BaseComponent {
79
79
  (child) => child !== styleElement && !(child instanceof HTMLSlotElement)
80
80
  );
81
81
 
82
+ // 调用子类的初始化钩子(无论是否渲染,都需要调用,因为组件已连接)
83
+ this.onConnected?.();
84
+
82
85
  // 如果有错误元素,需要重新渲染以恢复正常
83
86
  // 如果有实际内容且没有错误,跳过渲染(避免重复元素)
84
87
  if (hasActualContent && !hasErrorElement) {
@@ -109,9 +112,6 @@ export abstract class LightComponent extends BaseComponent {
109
112
  // 初始化事件监听器(无论是否渲染,都需要重新初始化,因为 DOM 可能被移动)
110
113
  this.initializeEventListeners();
111
114
 
112
- // 调用子类的初始化钩子(无论是否渲染,都需要调用,因为组件已连接)
113
- this.onConnected?.();
114
-
115
115
  // 如果进行了渲染,调用 onRendered 钩子
116
116
  if (hasActualContent === false || hasErrorElement) {
117
117
  // 使用 requestAnimationFrame 确保 DOM 已完全更新
@@ -251,12 +251,24 @@ export abstract class LightComponent extends BaseComponent {
251
251
  });
252
252
  oldChildren.forEach((child) => child.remove());
253
253
 
254
- // 确保样式元素在第一个位置
255
- if (stylesToApply && this.children.length > 1) {
256
- const styleElement = this.querySelector(
254
+ // 确保样式元素存在并在第一个位置
255
+ // 关键修复:在元素复用场景中,如果 _autoStyles 存在但样式元素被意外移除,需要重新创建
256
+ if (stylesToApply) {
257
+ let styleElement = this.querySelector(
257
258
  `style[data-wsx-light-component="${styleName}"]`
258
- );
259
- if (styleElement && styleElement !== this.firstChild) {
259
+ ) as HTMLStyleElement | null;
260
+
261
+ if (!styleElement) {
262
+ // 样式元素被意外移除,重新创建
263
+ styleElement = document.createElement("style");
264
+ styleElement.setAttribute("data-wsx-light-component", styleName);
265
+ styleElement.textContent = stylesToApply;
266
+ this.insertBefore(styleElement, this.firstChild);
267
+ } else if (styleElement.textContent !== stylesToApply) {
268
+ // 样式内容已变化,更新
269
+ styleElement.textContent = stylesToApply;
270
+ } else if (styleElement !== this.firstChild) {
271
+ // 样式元素存在但不在第一个位置,移动到第一个位置
260
272
  this.insertBefore(styleElement, this.firstChild);
261
273
  }
262
274
  }
@@ -1,4 +1,5 @@
1
1
  import { BaseComponent } from "./base-component";
2
+ import { resetCounterForNewRenderCycle } from "./utils/cache-key";
2
3
 
3
4
  /**
4
5
  * RenderContext
@@ -15,6 +16,9 @@ export class RenderContext {
15
16
  * @param fn The function to execute (usually the render method).
16
17
  */
17
18
  static runInContext<T>(component: BaseComponent, fn: () => T): T {
19
+ // 重置计数器以标记新的渲染周期开始
20
+ resetCounterForNewRenderCycle(component);
21
+
18
22
  const prev = RenderContext.current;
19
23
  RenderContext.current = component;
20
24
  try {
@@ -1,19 +1,13 @@
1
1
  /**
2
2
  * Cache Key Generation Utilities
3
3
  *
4
- * Pure functions for generating cache keys for DOM elements (RFC 0037).
4
+ * Pure functions for generating cache keys for DOM elements (RFC 0037 & RFC 0046).
5
5
  * These functions are used by the jsx-factory to identify and cache DOM elements.
6
6
  */
7
7
 
8
8
  import { RenderContext } from "../render-context";
9
9
  import type { BaseComponent } from "../base-component";
10
10
 
11
- /**
12
- * Internal symbol for position ID (used by Babel plugin in future)
13
- * For now, we use a string key for backward compatibility
14
- */
15
- const POSITION_ID_KEY = "__wsxPositionId";
16
-
17
11
  /**
18
12
  * Internal symbol for index (used in list scenarios)
19
13
  */
@@ -22,7 +16,7 @@ const INDEX_KEY = "__wsxIndex";
22
16
  /**
23
17
  * Component-level element counters (using WeakMap to avoid memory leaks)
24
18
  * Each component instance maintains its own counter to ensure unique cache keys
25
- * when position ID is not available.
19
+ * when a user-provided `key` is not available.
26
20
  */
27
21
  const componentElementCounters = new WeakMap<BaseComponent, number>();
28
22
 
@@ -37,15 +31,14 @@ const componentIdCache = new WeakMap<BaseComponent, string>();
37
31
  *
38
32
  * Cache key format: `${componentId}:${tag}:${identifier}`
39
33
  *
40
- * Priority:
41
- * 1. User-provided key (if exists) - most reliable
42
- * 2. Index (if in list scenario)
43
- * 3. Position ID (if provided and valid)
44
- * 4. Component-level counter (runtime fallback, ensures uniqueness)
45
- * 5. Timestamp fallback (last resort, ensures uniqueness)
34
+ * Priority (as per RFC 0048, following React/Vue model):
35
+ * 1. User-provided `key` (if exists) - most reliable
36
+ * 2. Index (if in list scenario, e.g., from `.map`)
37
+ * 3. Component-level counter (runtime fallback, ensures uniqueness per render)
38
+ * 4. Timestamp fallback (last resort, should rarely be hit)
46
39
  *
47
40
  * @param tag - HTML tag name
48
- * @param props - Element props (may contain position ID, index, or key)
41
+ * @param props - Element props (may contain index or key)
49
42
  * @param componentId - Component instance ID (from RenderContext)
50
43
  * @param component - Optional component instance (for counter-based fallback)
51
44
  * @returns Cache key string
@@ -56,11 +49,11 @@ export function generateCacheKey(
56
49
  componentId: string,
57
50
  component?: BaseComponent
58
51
  ): string {
59
- const positionId = props?.[POSITION_ID_KEY];
60
52
  const userKey = props?.key;
61
53
  const index = props?.[INDEX_KEY];
54
+ const positionId = props?.__wsxPositionId;
62
55
 
63
- // 优先级 1: 用户 key(最可靠)
56
+ // 优先级 1: 用户 key(最可靠, 符合 React/Vue 设计)
64
57
  if (userKey !== undefined && userKey !== null) {
65
58
  return `${componentId}:${tag}:key-${String(userKey)}`;
66
59
  }
@@ -70,12 +63,13 @@ export function generateCacheKey(
70
63
  return `${componentId}:${tag}:idx-${String(index)}`;
71
64
  }
72
65
 
73
- // 优先级 3: 位置 ID(编译时注入,如果有效)
74
- if (positionId !== undefined && positionId !== null && positionId !== "no-id") {
66
+ // 优先级 3: 位置 ID(由 babel 插件生成)
67
+ if (positionId !== undefined && positionId !== null) {
75
68
  return `${componentId}:${tag}:${String(positionId)}`;
76
69
  }
77
70
 
78
- // 优先级 4: 组件级别计数器(运行时回退,确保唯一性)
71
+ // 优先级 4: 组件级别计数器(运行时回退, 确保唯一性)
72
+ // 注意:计数器在 RenderContext.runInContext 开始时已重置
79
73
  if (component) {
80
74
  let counter = componentElementCounters.get(component) || 0;
81
75
  counter++;
@@ -83,10 +77,22 @@ export function generateCacheKey(
83
77
  return `${componentId}:${tag}:auto-${counter}`;
84
78
  }
85
79
 
86
- // 最后回退:时间戳(不推荐,但确保唯一性)
80
+ // 最后回退:时间戳(不推荐, 但确保唯一性)
87
81
  return `${componentId}:${tag}:fallback-${Date.now()}-${Math.random()}`;
88
82
  }
89
83
 
84
+ /**
85
+ * Resets the element counter for a component when a new render cycle starts.
86
+ * This should be called at the beginning of RenderContext.runInContext.
87
+ *
88
+ * @param component - The component instance starting a new render cycle
89
+ * @internal
90
+ */
91
+ export function resetCounterForNewRenderCycle(component: BaseComponent): void {
92
+ // 新的渲染周期开始, 直接重置计数器
93
+ componentElementCounters.set(component, 0);
94
+ }
95
+
90
96
  /**
91
97
  * Gets the component ID from the current render context.
92
98
  * Falls back to 'unknown' if no context is available.
@@ -48,7 +48,12 @@ function applySingleProp(
48
48
  // 处理事件监听器
49
49
  if (key.startsWith("on") && typeof value === "function") {
50
50
  const eventName = key.slice(2).toLowerCase();
51
+
52
+ // 关键修复:保存监听器引用,以便后续更新时移除旧的监听器
53
+ const listenerKey = `__wsxListener_${eventName}`;
51
54
  element.addEventListener(eventName, value as EventListener);
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ (element as any)[listenerKey] = value;
52
57
  return;
53
58
  }
54
59