@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/{chunk-ESZYREJK.mjs → chunk-5Q2VEEUH.mjs} +160 -35
- package/dist/index.js +226 -64
- package/dist/index.mjs +67 -30
- package/dist/jsx-runtime.js +160 -35
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +160 -35
- package/dist/jsx.mjs +1 -1
- package/package.json +2 -2
- package/src/base-component.ts +27 -0
- package/src/light-component.ts +20 -8
- package/src/render-context.ts +4 -0
- package/src/utils/cache-key.ts +27 -21
- package/src/utils/element-creation.ts +5 -0
- package/src/utils/element-update.ts +122 -45
- package/src/utils/update-children-helpers.ts +184 -18
- package/src/web-component.ts +72 -41
- package/dist/chunk-BPQGLNOQ.mjs +0 -1140
- package/dist/chunk-OGMB43J4.mjs +0 -1131
- package/dist/chunk-OXFZ575O.mjs +0 -1091
- package/dist/chunk-TKHKPLBM.mjs +0 -1142
- package/dist/tsconfig.tsbuildinfo +0 -1
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
|
|
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
|
-
|
|
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
|
|
702
|
-
|
|
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
|
-
|
|
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
|
-
|
|
716
|
-
|
|
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,
|
|
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
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
|
|
986
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wsxjs/wsx-core",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
51
|
+
"@wsxjs/wsx-logger": "0.0.25"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"tsup": "^8.0.0",
|
package/src/base-component.ts
CHANGED
|
@@ -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;
|
package/src/light-component.ts
CHANGED
|
@@ -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
|
-
|
|
256
|
-
|
|
254
|
+
// 确保样式元素存在并在第一个位置
|
|
255
|
+
// 关键修复:在元素复用场景中,如果 _autoStyles 存在但样式元素被意外移除,需要重新创建
|
|
256
|
+
if (stylesToApply) {
|
|
257
|
+
let styleElement = this.querySelector(
|
|
257
258
|
`style[data-wsx-light-component="${styleName}"]`
|
|
258
|
-
);
|
|
259
|
-
|
|
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
|
}
|
package/src/render-context.ts
CHANGED
|
@@ -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 {
|
package/src/utils/cache-key.ts
CHANGED
|
@@ -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
|
|
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.
|
|
44
|
-
* 4.
|
|
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
|
|
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
|
|
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
|
|