@wsxjs/wsx-core 0.0.22 → 0.0.23
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-BPQGLNOQ.mjs +1140 -0
- package/dist/chunk-ESZYREJK.mjs +1132 -0
- package/dist/chunk-OGMB43J4.mjs +1131 -0
- package/dist/chunk-TKHKPLBM.mjs +1142 -0
- package/dist/index.js +224 -183
- package/dist/index.mjs +1 -1
- package/dist/jsx-runtime.js +224 -183
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +224 -183
- package/dist/jsx.mjs +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +2 -2
- package/src/utils/element-update.ts +110 -304
- package/src/utils/update-children-helpers.ts +342 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper functions for updateChildren
|
|
3
|
+
*
|
|
4
|
+
* 将 updateChildren 的复杂逻辑拆分为小的纯函数,
|
|
5
|
+
* 遵循 Linus 的"好品味"原则:消除特殊情况,使代码简洁易读
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { flattenChildren, type JSXChildren } from "./dom-utils";
|
|
9
|
+
import { shouldPreserveElement, getElementCacheKey } from "./element-marking";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 收集所有需要保留的元素(第三方库注入的元素)
|
|
13
|
+
*/
|
|
14
|
+
export function collectPreservedElements(element: HTMLElement | SVGElement): Node[] {
|
|
15
|
+
const preserved: Node[] = [];
|
|
16
|
+
for (let i = 0; i < element.childNodes.length; i++) {
|
|
17
|
+
const child = element.childNodes[i];
|
|
18
|
+
if (shouldPreserveElement(child)) {
|
|
19
|
+
preserved.push(child);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return preserved;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 查找与 oldChild 对应的 DOM 节点(通过元素引用)
|
|
27
|
+
*/
|
|
28
|
+
function findDOMNodeByReference(
|
|
29
|
+
oldChild: HTMLElement | SVGElement,
|
|
30
|
+
parent: HTMLElement | SVGElement
|
|
31
|
+
): Node | null {
|
|
32
|
+
if (oldChild.parentNode === parent && !shouldPreserveElement(oldChild)) {
|
|
33
|
+
return oldChild;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 查找与 oldChild 对应的 DOM 节点(通过 cache key)
|
|
40
|
+
*/
|
|
41
|
+
function findDOMNodeByCacheKey(cacheKey: string, parent: HTMLElement | SVGElement): Node | null {
|
|
42
|
+
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
43
|
+
const child = parent.childNodes[i];
|
|
44
|
+
if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
45
|
+
if (shouldPreserveElement(child)) continue;
|
|
46
|
+
if (getElementCacheKey(child) === cacheKey) {
|
|
47
|
+
return child;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 查找元素节点对应的 DOM 节点
|
|
56
|
+
*/
|
|
57
|
+
export function findElementNode(
|
|
58
|
+
oldChild: HTMLElement | SVGElement,
|
|
59
|
+
parent: HTMLElement | SVGElement
|
|
60
|
+
): Node | null {
|
|
61
|
+
// 先尝试直接引用匹配
|
|
62
|
+
const byRef = findDOMNodeByReference(oldChild, parent);
|
|
63
|
+
if (byRef) return byRef;
|
|
64
|
+
|
|
65
|
+
// 再尝试 cache key 匹配
|
|
66
|
+
const cacheKey = getElementCacheKey(oldChild);
|
|
67
|
+
if (cacheKey) {
|
|
68
|
+
return findDOMNodeByCacheKey(cacheKey, parent);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 查找文本节点对应的 DOM 节点
|
|
76
|
+
* 关键:跳过所有元素节点,只查找文本节点
|
|
77
|
+
*/
|
|
78
|
+
export function findTextNode(
|
|
79
|
+
parent: HTMLElement | SVGElement,
|
|
80
|
+
domIndex: { value: number }
|
|
81
|
+
): Node | null {
|
|
82
|
+
while (domIndex.value < parent.childNodes.length) {
|
|
83
|
+
const node = parent.childNodes[domIndex.value];
|
|
84
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
85
|
+
const textNode = node;
|
|
86
|
+
domIndex.value++;
|
|
87
|
+
return textNode;
|
|
88
|
+
}
|
|
89
|
+
// 跳过元素节点和其他类型的节点(它们会在自己的迭代中处理)
|
|
90
|
+
// 关键:必须递增 domIndex,否则会无限循环
|
|
91
|
+
domIndex.value++;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 检查文本节点是否需要更新
|
|
98
|
+
*/
|
|
99
|
+
export function shouldUpdateTextNode(
|
|
100
|
+
oldText: string,
|
|
101
|
+
newText: string,
|
|
102
|
+
oldNode: Node | null
|
|
103
|
+
): boolean {
|
|
104
|
+
if (oldText !== newText) return true;
|
|
105
|
+
if (oldNode && oldNode.nodeType === Node.TEXT_NODE && oldNode.textContent !== newText) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 更新或创建文本节点
|
|
113
|
+
*/
|
|
114
|
+
export function updateOrCreateTextNode(
|
|
115
|
+
parent: HTMLElement | SVGElement,
|
|
116
|
+
oldNode: Node | null,
|
|
117
|
+
newText: string
|
|
118
|
+
): void {
|
|
119
|
+
if (oldNode && oldNode.nodeType === Node.TEXT_NODE) {
|
|
120
|
+
// 只有当文本内容不同时才更新
|
|
121
|
+
if (oldNode.textContent !== newText) {
|
|
122
|
+
oldNode.textContent = newText;
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// Bug 2 修复:如果 oldNode 为 null,说明 findTextNode 没有找到对应的文本节点
|
|
126
|
+
// 此时不应该盲目更新第一个找到的文本节点,而应该创建新节点
|
|
127
|
+
// 因为:
|
|
128
|
+
// 1. 如果文本内容相同,调用方已经跳过了更新(在 element-update.ts 中)
|
|
129
|
+
// 2. 如果文本内容不同,应该创建新节点,而不是更新错误的节点
|
|
130
|
+
const newTextNode = document.createTextNode(newText);
|
|
131
|
+
if (oldNode && !shouldPreserveElement(oldNode)) {
|
|
132
|
+
parent.replaceChild(newTextNode, oldNode);
|
|
133
|
+
} else {
|
|
134
|
+
parent.insertBefore(newTextNode, oldNode || null);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 移除节点(如果不应该保留)
|
|
141
|
+
*/
|
|
142
|
+
export function removeNodeIfNotPreserved(
|
|
143
|
+
parent: HTMLElement | SVGElement,
|
|
144
|
+
node: Node | null
|
|
145
|
+
): void {
|
|
146
|
+
if (node && !shouldPreserveElement(node) && node.parentNode === parent) {
|
|
147
|
+
parent.removeChild(node);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 替换或插入元素节点
|
|
153
|
+
*/
|
|
154
|
+
export function replaceOrInsertElement(
|
|
155
|
+
parent: HTMLElement | SVGElement,
|
|
156
|
+
newChild: HTMLElement | SVGElement,
|
|
157
|
+
oldNode: Node | null
|
|
158
|
+
): void {
|
|
159
|
+
// 如果新元素已经在其他父元素中,先移除
|
|
160
|
+
if (newChild.parentNode && newChild.parentNode !== parent) {
|
|
161
|
+
newChild.parentNode.removeChild(newChild);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (oldNode && !shouldPreserveElement(oldNode)) {
|
|
165
|
+
if (oldNode !== newChild) {
|
|
166
|
+
parent.replaceChild(newChild, oldNode);
|
|
167
|
+
}
|
|
168
|
+
} else if (newChild.parentNode !== parent) {
|
|
169
|
+
parent.insertBefore(newChild, oldNode || null);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 添加新的子节点到末尾
|
|
175
|
+
*/
|
|
176
|
+
export function appendNewChild(parent: HTMLElement | SVGElement, child: JSXChildren): void {
|
|
177
|
+
if (child === null || child === undefined || child === false) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
182
|
+
parent.appendChild(document.createTextNode(String(child)));
|
|
183
|
+
} else if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
184
|
+
// 确保元素不在其他父元素中
|
|
185
|
+
if (child.parentNode && child.parentNode !== parent) {
|
|
186
|
+
child.parentNode.removeChild(child);
|
|
187
|
+
}
|
|
188
|
+
if (child.parentNode !== parent) {
|
|
189
|
+
parent.appendChild(child);
|
|
190
|
+
}
|
|
191
|
+
} else if (child instanceof DocumentFragment) {
|
|
192
|
+
parent.appendChild(child);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 构建新子元素的引用集合和 cache key 映射
|
|
198
|
+
*/
|
|
199
|
+
export function buildNewChildrenMaps(flatNew: JSXChildren[]): {
|
|
200
|
+
elementSet: Set<HTMLElement | SVGElement | DocumentFragment>;
|
|
201
|
+
cacheKeyMap: Map<string, HTMLElement | SVGElement>;
|
|
202
|
+
} {
|
|
203
|
+
const elementSet = new Set<HTMLElement | SVGElement | DocumentFragment>();
|
|
204
|
+
const cacheKeyMap = new Map<string, HTMLElement | SVGElement>();
|
|
205
|
+
|
|
206
|
+
for (const child of flatNew) {
|
|
207
|
+
if (
|
|
208
|
+
child instanceof HTMLElement ||
|
|
209
|
+
child instanceof SVGElement ||
|
|
210
|
+
child instanceof DocumentFragment
|
|
211
|
+
) {
|
|
212
|
+
elementSet.add(child);
|
|
213
|
+
if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
214
|
+
const cacheKey = getElementCacheKey(child);
|
|
215
|
+
if (cacheKey) {
|
|
216
|
+
cacheKeyMap.set(cacheKey, child);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { elementSet, cacheKeyMap };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 检查节点是否应该被移除
|
|
227
|
+
*/
|
|
228
|
+
export function shouldRemoveNode(
|
|
229
|
+
node: Node,
|
|
230
|
+
elementSet: Set<HTMLElement | SVGElement | DocumentFragment>,
|
|
231
|
+
cacheKeyMap: Map<string, HTMLElement | SVGElement>
|
|
232
|
+
): boolean {
|
|
233
|
+
// 保留的元素不移除
|
|
234
|
+
if (shouldPreserveElement(node)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 检查是否在新子元素集合中(通过引用)
|
|
239
|
+
if (
|
|
240
|
+
node instanceof HTMLElement ||
|
|
241
|
+
node instanceof SVGElement ||
|
|
242
|
+
node instanceof DocumentFragment
|
|
243
|
+
) {
|
|
244
|
+
if (elementSet.has(node)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 检查是否通过 cache key 匹配
|
|
249
|
+
if (node instanceof HTMLElement || node instanceof SVGElement) {
|
|
250
|
+
const cacheKey = getElementCacheKey(node);
|
|
251
|
+
if (cacheKey && cacheKeyMap.has(cacheKey)) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 处理重复的 cache key(如果 DOM 中有多个元素具有相同的 cache key,只保留 newChild)
|
|
262
|
+
* 这个函数确保每个 cache key 在 DOM 中只出现一次
|
|
263
|
+
*/
|
|
264
|
+
export function deduplicateCacheKeys(
|
|
265
|
+
parent: HTMLElement | SVGElement,
|
|
266
|
+
cacheKeyMap: Map<string, HTMLElement | SVGElement>
|
|
267
|
+
): void {
|
|
268
|
+
const processedCacheKeys = new Set<string>();
|
|
269
|
+
|
|
270
|
+
// 从后往前遍历,避免在循环中修改 DOM 导致索引问题
|
|
271
|
+
for (let i = parent.childNodes.length - 1; i >= 0; i--) {
|
|
272
|
+
const child = parent.childNodes[i];
|
|
273
|
+
if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
274
|
+
// 跳过应该保留的元素(第三方库注入的元素)
|
|
275
|
+
if (shouldPreserveElement(child)) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const cacheKey = getElementCacheKey(child);
|
|
279
|
+
if (cacheKey && cacheKeyMap.has(cacheKey) && !processedCacheKeys.has(cacheKey)) {
|
|
280
|
+
processedCacheKeys.add(cacheKey);
|
|
281
|
+
const newChild = cacheKeyMap.get(cacheKey)!;
|
|
282
|
+
// 如果 child 不是 newChild,说明是旧元素,应该被替换
|
|
283
|
+
if (child !== newChild) {
|
|
284
|
+
parent.replaceChild(newChild, child);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 收集需要移除的节点
|
|
293
|
+
*/
|
|
294
|
+
export function collectNodesToRemove(
|
|
295
|
+
parent: HTMLElement | SVGElement,
|
|
296
|
+
elementSet: Set<HTMLElement | SVGElement | DocumentFragment>,
|
|
297
|
+
cacheKeyMap: Map<string, HTMLElement | SVGElement>
|
|
298
|
+
): Node[] {
|
|
299
|
+
const nodesToRemove: Node[] = [];
|
|
300
|
+
|
|
301
|
+
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
302
|
+
const node = parent.childNodes[i];
|
|
303
|
+
if (shouldRemoveNode(node, elementSet, cacheKeyMap)) {
|
|
304
|
+
nodesToRemove.push(node);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return nodesToRemove;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 批量移除节点(从后往前,避免索引变化)
|
|
313
|
+
*/
|
|
314
|
+
export function removeNodes(parent: HTMLElement | SVGElement, nodes: Node[]): void {
|
|
315
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
316
|
+
const node = nodes[i];
|
|
317
|
+
if (node.parentNode === parent) {
|
|
318
|
+
parent.removeChild(node);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 重新插入保留的元素到 DOM 末尾
|
|
325
|
+
*/
|
|
326
|
+
export function reinsertPreservedElements(
|
|
327
|
+
parent: HTMLElement | SVGElement,
|
|
328
|
+
preservedElements: Node[]
|
|
329
|
+
): void {
|
|
330
|
+
for (const element of preservedElements) {
|
|
331
|
+
if (element.parentNode !== parent) {
|
|
332
|
+
parent.appendChild(element);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 扁平化子元素
|
|
339
|
+
*/
|
|
340
|
+
export function flattenChildrenSafe(children: JSXChildren[]): JSXChildren[] {
|
|
341
|
+
return flattenChildren(children);
|
|
342
|
+
}
|