@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.
- package/dist/{chunk-OXFZ575O.mjs → chunk-5Q2VEEUH.mjs} +352 -186
- package/dist/index.js +418 -215
- package/dist/index.mjs +67 -30
- package/dist/jsx-runtime.js +352 -186
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +352 -186
- package/dist/jsx.mjs +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- 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 +192 -309
- package/src/utils/update-children-helpers.ts +508 -0
- package/src/web-component.ts +72 -41
|
@@ -0,0 +1,508 @@
|
|
|
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
|
+
// 关键修复:只检查直接子文本节点,确保 node.parentNode === parent
|
|
85
|
+
// 这样可以防止将元素内部的文本节点(如 span 内部的文本节点)误判为独立的文本节点
|
|
86
|
+
if (node.nodeType === Node.TEXT_NODE && node.parentNode === parent) {
|
|
87
|
+
const textNode = node;
|
|
88
|
+
domIndex.value++;
|
|
89
|
+
return textNode;
|
|
90
|
+
}
|
|
91
|
+
// 跳过元素节点和其他类型的节点(它们会在自己的迭代中处理)
|
|
92
|
+
// 关键:必须递增 domIndex,否则会无限循环
|
|
93
|
+
domIndex.value++;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 检查文本节点是否需要更新
|
|
100
|
+
*/
|
|
101
|
+
export function shouldUpdateTextNode(
|
|
102
|
+
oldText: string,
|
|
103
|
+
newText: string,
|
|
104
|
+
oldNode: Node | null
|
|
105
|
+
): boolean {
|
|
106
|
+
if (oldText !== newText) return true;
|
|
107
|
+
if (oldNode && oldNode.nodeType === Node.TEXT_NODE && oldNode.textContent !== newText) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 更新或创建文本节点
|
|
115
|
+
* @returns 更新后的文本节点(用于标记为已处理)
|
|
116
|
+
*/
|
|
117
|
+
export function updateOrCreateTextNode(
|
|
118
|
+
parent: HTMLElement | SVGElement,
|
|
119
|
+
oldNode: Node | null,
|
|
120
|
+
newText: string
|
|
121
|
+
): Node {
|
|
122
|
+
if (oldNode && oldNode.nodeType === Node.TEXT_NODE) {
|
|
123
|
+
// 只有当文本内容不同时才更新
|
|
124
|
+
if (oldNode.textContent !== newText) {
|
|
125
|
+
oldNode.textContent = newText;
|
|
126
|
+
}
|
|
127
|
+
return oldNode;
|
|
128
|
+
} else {
|
|
129
|
+
// RFC 0048 关键修复:如果 oldNode 为 null,先检查是否已经存在相同内容的直接子文本节点
|
|
130
|
+
// 这样可以防止重复创建文本节点
|
|
131
|
+
// 只检查直接子文本节点,不检查元素内部的文本节点
|
|
132
|
+
if (!oldNode) {
|
|
133
|
+
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
134
|
+
const node = parent.childNodes[i];
|
|
135
|
+
// 关键修复:只检查直接子文本节点,确保 node.parentNode === parent
|
|
136
|
+
// 这样可以防止将元素内部的文本节点(如 span 内部的文本节点)误判为独立的文本节点
|
|
137
|
+
if (
|
|
138
|
+
node.nodeType === Node.TEXT_NODE &&
|
|
139
|
+
node.parentNode === parent &&
|
|
140
|
+
node.textContent === newText
|
|
141
|
+
) {
|
|
142
|
+
// 找到相同内容的直接子文本节点,返回它而不是创建新节点
|
|
143
|
+
return node;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 如果没有找到相同内容的文本节点,创建新节点
|
|
149
|
+
const newTextNode = document.createTextNode(newText);
|
|
150
|
+
if (oldNode && !shouldPreserveElement(oldNode)) {
|
|
151
|
+
parent.replaceChild(newTextNode, oldNode);
|
|
152
|
+
} else {
|
|
153
|
+
parent.insertBefore(newTextNode, oldNode || null);
|
|
154
|
+
}
|
|
155
|
+
return newTextNode;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 移除节点(如果不应该保留)
|
|
161
|
+
*/
|
|
162
|
+
export function removeNodeIfNotPreserved(
|
|
163
|
+
parent: HTMLElement | SVGElement,
|
|
164
|
+
node: Node | null
|
|
165
|
+
): void {
|
|
166
|
+
if (node && !shouldPreserveElement(node) && node.parentNode === parent) {
|
|
167
|
+
parent.removeChild(node);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 替换或插入元素节点
|
|
173
|
+
*/
|
|
174
|
+
export function replaceOrInsertElement(
|
|
175
|
+
parent: HTMLElement | SVGElement,
|
|
176
|
+
newChild: HTMLElement | SVGElement,
|
|
177
|
+
oldNode: Node | null
|
|
178
|
+
): void {
|
|
179
|
+
// 确定目标位置:
|
|
180
|
+
// - 如果 oldNode 是保留元素,应该在 oldNode 之前插入(targetNextSibling = oldNode)
|
|
181
|
+
// - 否则,应该在 oldNode 的位置(oldNode.nextSibling 之前)
|
|
182
|
+
const targetNextSibling =
|
|
183
|
+
oldNode && shouldPreserveElement(oldNode) ? oldNode : oldNode?.nextSibling || null;
|
|
184
|
+
replaceOrInsertElementAtPosition(parent, newChild, oldNode, targetNextSibling);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 替换或插入元素节点到指定位置
|
|
189
|
+
* 确保元素在正确的位置,保持 DOM 顺序
|
|
190
|
+
*/
|
|
191
|
+
export function replaceOrInsertElementAtPosition(
|
|
192
|
+
parent: HTMLElement | SVGElement,
|
|
193
|
+
newChild: HTMLElement | SVGElement,
|
|
194
|
+
oldNode: Node | null,
|
|
195
|
+
targetNextSibling: Node | null
|
|
196
|
+
): void {
|
|
197
|
+
// 如果新元素已经在其他父元素中,先移除
|
|
198
|
+
if (newChild.parentNode && newChild.parentNode !== parent) {
|
|
199
|
+
newChild.parentNode.removeChild(newChild);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 检查元素是否已经在正确位置
|
|
203
|
+
const isInCorrectPosition =
|
|
204
|
+
newChild.parentNode === parent && newChild.nextSibling === targetNextSibling;
|
|
205
|
+
|
|
206
|
+
if (isInCorrectPosition) {
|
|
207
|
+
// 已经在正确位置,不需要移动
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 如果元素已经在 parent 中但位置不对,需要移动到正确位置
|
|
212
|
+
if (newChild.parentNode === parent) {
|
|
213
|
+
// insertBefore 会自动处理:如果元素已经在 DOM 中,会先移除再插入到新位置
|
|
214
|
+
parent.insertBefore(newChild, targetNextSibling);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 元素不在 parent 中,需要插入或替换
|
|
219
|
+
if (oldNode && oldNode.parentNode === parent && !shouldPreserveElement(oldNode)) {
|
|
220
|
+
if (oldNode !== newChild) {
|
|
221
|
+
// 关键修复:在替换元素之前,标记旧元素内的所有文本节点为已处理
|
|
222
|
+
// 这样可以防止在移除阶段被误删
|
|
223
|
+
// 注意:这里只标记直接子文本节点,不递归处理嵌套元素内的文本节点
|
|
224
|
+
// 因为 replaceChild 会一起处理元素及其所有子节点
|
|
225
|
+
parent.replaceChild(newChild, oldNode);
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
// RFC 0048 关键修复:在插入元素之前,检查是否已经存在相同内容的元素
|
|
229
|
+
// 如果 newChild 已经在 parent 中,不应该再次插入
|
|
230
|
+
// 如果 parent 中已经存在相同标签名和内容的元素,也不应该插入
|
|
231
|
+
// 这样可以防止重复插入相同的元素(特别是从 HTML 字符串解析而来的元素)
|
|
232
|
+
if (newChild.parentNode === parent) {
|
|
233
|
+
// 元素已经在 parent 中,不需要再次插入
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// RFC 0048 关键修复:检查是否已经存在相同内容的元素
|
|
237
|
+
// 注意:这个检查只适用于从 HTML 字符串解析而来的元素(没有 __wsxCacheKey)
|
|
238
|
+
// 对于由 h() 创建的元素(有 __wsxCacheKey),应该通过引用匹配,而不是内容匹配
|
|
239
|
+
// 这样可以避免误判语言切换器等组件(它们由 h() 创建,每次渲染可能有相同内容但不同引用)
|
|
240
|
+
const newChildCacheKey = getElementCacheKey(newChild);
|
|
241
|
+
// 只有当 newChild 没有 cache key 时,才进行内容匹配检查
|
|
242
|
+
// 如果有 cache key,说明是由 h() 创建的,应该通过引用匹配(已经在 elementSet 中检查)
|
|
243
|
+
if (!newChildCacheKey) {
|
|
244
|
+
const newChildContent = newChild.textContent || "";
|
|
245
|
+
const newChildTag = newChild.tagName.toLowerCase();
|
|
246
|
+
// 检查 parent 中是否已经存在相同标签名和内容的元素(且也没有 cache key)
|
|
247
|
+
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
248
|
+
const existingNode = parent.childNodes[i];
|
|
249
|
+
if (existingNode instanceof HTMLElement || existingNode instanceof SVGElement) {
|
|
250
|
+
const existingCacheKey = getElementCacheKey(existingNode);
|
|
251
|
+
// 只有当 existingNode 也没有 cache key 时,才进行内容匹配
|
|
252
|
+
if (
|
|
253
|
+
!existingCacheKey &&
|
|
254
|
+
existingNode.tagName.toLowerCase() === newChildTag &&
|
|
255
|
+
existingNode.textContent === newChildContent &&
|
|
256
|
+
existingNode !== newChild
|
|
257
|
+
) {
|
|
258
|
+
// 找到相同内容的元素(且都没有 cache key),不需要插入 newChild
|
|
259
|
+
// 这是从 HTML 字符串解析而来的重复元素
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// 插入到目标位置
|
|
266
|
+
parent.insertBefore(newChild, targetNextSibling);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 添加新的子节点到末尾
|
|
272
|
+
*/
|
|
273
|
+
export function appendNewChild(
|
|
274
|
+
parent: HTMLElement | SVGElement,
|
|
275
|
+
child: JSXChildren,
|
|
276
|
+
processedNodes?: Set<Node>
|
|
277
|
+
): void {
|
|
278
|
+
if (child === null || child === undefined || child === false) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
283
|
+
const newTextNode = document.createTextNode(String(child));
|
|
284
|
+
parent.appendChild(newTextNode);
|
|
285
|
+
// 关键修复:标记新创建的文本节点为已处理,防止在移除阶段被误删
|
|
286
|
+
if (processedNodes) {
|
|
287
|
+
processedNodes.add(newTextNode);
|
|
288
|
+
}
|
|
289
|
+
} else if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
290
|
+
// RFC 0048 关键修复:在插入元素之前,检查是否已经存在相同的元素
|
|
291
|
+
// 如果 child 已经在 parent 中,不应该再次插入
|
|
292
|
+
// 这样可以防止重复插入相同的元素(特别是从 HTML 字符串解析而来的元素)
|
|
293
|
+
if (child.parentNode === parent) {
|
|
294
|
+
// 元素已经在 parent 中,不需要再次插入
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// 确保元素不在其他父元素中
|
|
298
|
+
if (child.parentNode && child.parentNode !== parent) {
|
|
299
|
+
child.parentNode.removeChild(child);
|
|
300
|
+
}
|
|
301
|
+
// 插入到末尾
|
|
302
|
+
parent.appendChild(child);
|
|
303
|
+
// RFC 0048 关键修复:标记新添加的元素为已处理,防止在移除阶段被误删
|
|
304
|
+
if (processedNodes) {
|
|
305
|
+
processedNodes.add(child);
|
|
306
|
+
}
|
|
307
|
+
} else if (child instanceof DocumentFragment) {
|
|
308
|
+
parent.appendChild(child);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 构建新子元素的引用集合和 cache key 映射
|
|
314
|
+
*/
|
|
315
|
+
export function buildNewChildrenMaps(flatNew: JSXChildren[]): {
|
|
316
|
+
elementSet: Set<HTMLElement | SVGElement | DocumentFragment>;
|
|
317
|
+
cacheKeyMap: Map<string, HTMLElement | SVGElement>;
|
|
318
|
+
} {
|
|
319
|
+
const elementSet = new Set<HTMLElement | SVGElement | DocumentFragment>();
|
|
320
|
+
const cacheKeyMap = new Map<string, HTMLElement | SVGElement>();
|
|
321
|
+
|
|
322
|
+
for (const child of flatNew) {
|
|
323
|
+
if (
|
|
324
|
+
child instanceof HTMLElement ||
|
|
325
|
+
child instanceof SVGElement ||
|
|
326
|
+
child instanceof DocumentFragment
|
|
327
|
+
) {
|
|
328
|
+
elementSet.add(child);
|
|
329
|
+
if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
330
|
+
const cacheKey = getElementCacheKey(child);
|
|
331
|
+
if (cacheKey) {
|
|
332
|
+
cacheKeyMap.set(cacheKey, child);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { elementSet, cacheKeyMap };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 检查节点是否应该被移除
|
|
343
|
+
*/
|
|
344
|
+
export function shouldRemoveNode(
|
|
345
|
+
node: Node,
|
|
346
|
+
elementSet: Set<HTMLElement | SVGElement | DocumentFragment>,
|
|
347
|
+
cacheKeyMap: Map<string, HTMLElement | SVGElement>,
|
|
348
|
+
processedNodes?: Set<Node>
|
|
349
|
+
): boolean {
|
|
350
|
+
// 保留的元素不移除
|
|
351
|
+
if (shouldPreserveElement(node)) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 关键修复:如果文本节点已被处理,不应该移除
|
|
356
|
+
if (node.nodeType === Node.TEXT_NODE && processedNodes && processedNodes.has(node)) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 关键修复:如果文本节点在已处理的元素内部,不应该移除
|
|
361
|
+
// 这样可以防止在元素被替换时,元素内部的文本节点被误删
|
|
362
|
+
// 但是,只有当父元素还在当前 parent 的子树中时才检查
|
|
363
|
+
if (node.nodeType === Node.TEXT_NODE && processedNodes) {
|
|
364
|
+
let parent = node.parentNode;
|
|
365
|
+
while (parent) {
|
|
366
|
+
// 关键修复:只有当父元素还在 DOM 中并且在当前 parent 的子树中时才检查
|
|
367
|
+
// 因为如果父元素被 replaceChild 移除了,它就不在 DOM 中了
|
|
368
|
+
if (processedNodes.has(parent) && parent.parentNode) {
|
|
369
|
+
return false; // 文本节点在已处理的元素内部,不应该移除
|
|
370
|
+
}
|
|
371
|
+
parent = parent.parentNode;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 检查是否在新子元素集合中(通过引用)
|
|
376
|
+
if (
|
|
377
|
+
node instanceof HTMLElement ||
|
|
378
|
+
node instanceof SVGElement ||
|
|
379
|
+
node instanceof DocumentFragment
|
|
380
|
+
) {
|
|
381
|
+
if (elementSet.has(node)) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 检查是否通过 cache key 匹配
|
|
386
|
+
if (node instanceof HTMLElement || node instanceof SVGElement) {
|
|
387
|
+
const cacheKey = getElementCacheKey(node);
|
|
388
|
+
if (cacheKey && cacheKeyMap.has(cacheKey)) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 处理重复的 cache key(如果 DOM 中有多个元素具有相同的 cache key,只保留 newChild)
|
|
399
|
+
* 这个函数确保每个 cache key 在 DOM 中只出现一次
|
|
400
|
+
*/
|
|
401
|
+
export function deduplicateCacheKeys(
|
|
402
|
+
parent: HTMLElement | SVGElement,
|
|
403
|
+
cacheKeyMap: Map<string, HTMLElement | SVGElement>
|
|
404
|
+
): void {
|
|
405
|
+
const processedCacheKeys = new Set<string>();
|
|
406
|
+
|
|
407
|
+
// 从后往前遍历,避免在循环中修改 DOM 导致索引问题
|
|
408
|
+
for (let i = parent.childNodes.length - 1; i >= 0; i--) {
|
|
409
|
+
const child = parent.childNodes[i];
|
|
410
|
+
if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
411
|
+
// 跳过应该保留的元素(第三方库注入的元素)
|
|
412
|
+
if (shouldPreserveElement(child)) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const cacheKey = getElementCacheKey(child);
|
|
416
|
+
if (cacheKey && cacheKeyMap.has(cacheKey) && !processedCacheKeys.has(cacheKey)) {
|
|
417
|
+
processedCacheKeys.add(cacheKey);
|
|
418
|
+
const newChild = cacheKeyMap.get(cacheKey)!;
|
|
419
|
+
// 如果 child 不是 newChild,说明是旧元素,应该被替换
|
|
420
|
+
if (child !== newChild) {
|
|
421
|
+
parent.replaceChild(newChild, child);
|
|
422
|
+
}
|
|
423
|
+
// 如果 child === newChild,说明 newChild 已经在正确位置,不需要操作
|
|
424
|
+
} else if (cacheKey && cacheKeyMap.has(cacheKey) && processedCacheKeys.has(cacheKey)) {
|
|
425
|
+
// 如果 cache key 已经被处理过,说明有重复,应该移除这个旧元素
|
|
426
|
+
// 但是,只有当 child 不是 newChild 时才移除
|
|
427
|
+
const newChild = cacheKeyMap.get(cacheKey)!;
|
|
428
|
+
if (child !== newChild) {
|
|
429
|
+
parent.removeChild(child);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* 收集需要移除的节点
|
|
438
|
+
*/
|
|
439
|
+
export function collectNodesToRemove(
|
|
440
|
+
parent: HTMLElement | SVGElement,
|
|
441
|
+
elementSet: Set<HTMLElement | SVGElement | DocumentFragment>,
|
|
442
|
+
cacheKeyMap: Map<string, HTMLElement | SVGElement>,
|
|
443
|
+
processedNodes?: Set<Node>
|
|
444
|
+
): Node[] {
|
|
445
|
+
const nodesToRemove: Node[] = [];
|
|
446
|
+
|
|
447
|
+
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
448
|
+
const node = parent.childNodes[i];
|
|
449
|
+
if (shouldRemoveNode(node, elementSet, cacheKeyMap, processedNodes)) {
|
|
450
|
+
nodesToRemove.push(node);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return nodesToRemove;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 批量移除节点(从后往前,避免索引变化)
|
|
459
|
+
* @param parent - 父元素
|
|
460
|
+
* @param nodes - 要移除的节点列表
|
|
461
|
+
* @param cacheManager - 可选的缓存管理器,用于获取元素的 ref 回调
|
|
462
|
+
*/
|
|
463
|
+
export function removeNodes(
|
|
464
|
+
parent: HTMLElement | SVGElement,
|
|
465
|
+
nodes: Node[],
|
|
466
|
+
cacheManager?: { getMetadata: (element: Element) => Record<string, unknown> | undefined }
|
|
467
|
+
): void {
|
|
468
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
469
|
+
const node = nodes[i];
|
|
470
|
+
if (node.parentNode === parent) {
|
|
471
|
+
// 关键修复:在移除元素之前,检查是否有 ref 回调,如果有,调用它并传入 null
|
|
472
|
+
// 这确保组件可以清理引用,避免使用已移除的元素
|
|
473
|
+
if (cacheManager && (node instanceof HTMLElement || node instanceof SVGElement)) {
|
|
474
|
+
const metadata = cacheManager.getMetadata(node);
|
|
475
|
+
const refCallback = metadata?.ref;
|
|
476
|
+
if (typeof refCallback === "function") {
|
|
477
|
+
try {
|
|
478
|
+
refCallback(null);
|
|
479
|
+
} catch {
|
|
480
|
+
// 忽略回调错误
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
parent.removeChild(node);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* 重新插入保留的元素到 DOM 末尾
|
|
491
|
+
*/
|
|
492
|
+
export function reinsertPreservedElements(
|
|
493
|
+
parent: HTMLElement | SVGElement,
|
|
494
|
+
preservedElements: Node[]
|
|
495
|
+
): void {
|
|
496
|
+
for (const element of preservedElements) {
|
|
497
|
+
if (element.parentNode !== parent) {
|
|
498
|
+
parent.appendChild(element);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* 扁平化子元素
|
|
505
|
+
*/
|
|
506
|
+
export function flattenChildrenSafe(children: JSXChildren[]): JSXChildren[] {
|
|
507
|
+
return flattenChildren(children);
|
|
508
|
+
}
|
package/src/web-component.ts
CHANGED
|
@@ -79,6 +79,9 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
79
79
|
const hasActualContent =
|
|
80
80
|
allChildren.length > styleElements.length + slotElements.length;
|
|
81
81
|
|
|
82
|
+
// 调用子类的初始化钩子
|
|
83
|
+
this.onConnected?.();
|
|
84
|
+
|
|
82
85
|
// 如果有错误元素,需要重新渲染以恢复正常
|
|
83
86
|
// 如果有实际内容且没有错误,跳过渲染(避免重复元素)
|
|
84
87
|
if (hasActualContent && !hasErrorElement) {
|
|
@@ -110,9 +113,6 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
110
113
|
// 初始化事件监听器
|
|
111
114
|
this.initializeEventListeners();
|
|
112
115
|
|
|
113
|
-
// 调用子类的初始化钩子
|
|
114
|
-
this.onConnected?.();
|
|
115
|
-
|
|
116
116
|
// 如果进行了渲染,调用 onRendered 钩子
|
|
117
117
|
if (hasActualContent === false || hasErrorElement) {
|
|
118
118
|
// 使用 requestAnimationFrame 确保 DOM 已完全更新
|
|
@@ -173,12 +173,21 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
173
173
|
const focusState = this.captureFocusState();
|
|
174
174
|
this._pendingFocusState = focusState;
|
|
175
175
|
|
|
176
|
-
// 2. 保存当前的 adopted stylesheets
|
|
176
|
+
// 2. 保存当前的 adopted stylesheets 并检测实际的样式状态
|
|
177
177
|
const adoptedStyleSheets = this.shadowRoot.adoptedStyleSheets || [];
|
|
178
|
+
// 自动检测模式:检查实际的 ShadowRoot 样式状态,而不仅仅是保存的数组
|
|
179
|
+
// 这样可以更准确地检测样式是否真的已应用
|
|
180
|
+
const hasActualAdoptedStyles =
|
|
181
|
+
this.shadowRoot.adoptedStyleSheets && this.shadowRoot.adoptedStyleSheets.length > 0;
|
|
182
|
+
// 检查 fallback 模式的样式元素
|
|
183
|
+
const hasFallbackStyleElement = Array.from(this.shadowRoot.children).some(
|
|
184
|
+
(child) => child instanceof HTMLStyleElement
|
|
185
|
+
);
|
|
178
186
|
|
|
179
187
|
try {
|
|
180
|
-
// 3.
|
|
181
|
-
|
|
188
|
+
// 3. 自动检测模式:只有在没有实际样式时才重新应用样式
|
|
189
|
+
// 检查 adoptedStyleSheets 和 fallback 样式元素
|
|
190
|
+
if (!hasActualAdoptedStyles && !hasFallbackStyleElement) {
|
|
182
191
|
const stylesToApply = this._autoStyles || this.config.styles;
|
|
183
192
|
if (stylesToApply) {
|
|
184
193
|
const styleName = this.config.styleName || this.constructor.name;
|
|
@@ -205,46 +214,68 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
205
214
|
}
|
|
206
215
|
}
|
|
207
216
|
|
|
208
|
-
// 6.
|
|
209
|
-
|
|
217
|
+
// 6. 执行 DOM 操作(同步,不使用 RAF,因为已经在 scheduleRerender 的 RAF 中)
|
|
218
|
+
// 关键修复 (RFC-0042):检查 content 是否已经在 shadowRoot 中(元素复用场景)
|
|
219
|
+
// 如果 content 已经在 shadowRoot 中,不需要再次添加
|
|
220
|
+
// 这样可以避免移动元素,导致文本节点更新丢失
|
|
221
|
+
const isContentAlreadyInShadowRoot = content.parentNode === this.shadowRoot;
|
|
222
|
+
|
|
223
|
+
if (!isContentAlreadyInShadowRoot) {
|
|
224
|
+
// 添加新内容(仅在不在 shadowRoot 中时)
|
|
225
|
+
this.shadowRoot.appendChild(content);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 移除旧内容(保留样式元素和未标记元素)
|
|
229
|
+
// 关键修复:使用 shouldPreserveElement() 来保护第三方库注入的元素
|
|
230
|
+
const oldChildren = Array.from(this.shadowRoot.children).filter((child) => {
|
|
231
|
+
// 保留新添加的内容(或已经在 shadowRoot 中的 content)
|
|
232
|
+
if (child === content) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
// 保留样式元素
|
|
236
|
+
if (child instanceof HTMLStyleElement) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
// 保留未标记的元素(第三方库注入的元素、自定义元素)
|
|
240
|
+
// 这是 RFC 0037 Phase 5 的核心:保护未标记元素
|
|
241
|
+
if (shouldPreserveElement(child)) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
});
|
|
246
|
+
oldChildren.forEach((child) => child.remove());
|
|
247
|
+
|
|
248
|
+
// 7. 恢复 adopted stylesheets(在 DOM 操作之后,确保样式不被意外移除)
|
|
249
|
+
// 关键修复:在 DOM 操作之后恢复样式,防止样式在 DOM 操作过程中被意外清空
|
|
250
|
+
// 自动检测模式:检查实际的样式状态,确保样式正确恢复
|
|
251
|
+
const hasStylesAfterDOM =
|
|
252
|
+
this.shadowRoot.adoptedStyleSheets && this.shadowRoot.adoptedStyleSheets.length > 0;
|
|
253
|
+
const hasStyleElementAfterDOM = Array.from(this.shadowRoot.children).some(
|
|
254
|
+
(child) => child instanceof HTMLStyleElement
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (adoptedStyleSheets.length > 0) {
|
|
258
|
+
// 恢复保存的 adoptedStyleSheets
|
|
210
259
|
this.shadowRoot.adoptedStyleSheets = adoptedStyleSheets;
|
|
260
|
+
} else if (!hasStylesAfterDOM && !hasStyleElementAfterDOM) {
|
|
261
|
+
// 自动检测模式:如果 DOM 操作后没有样式,自动重新应用(防止样式丢失)
|
|
262
|
+
// 关键修复:在元素复用场景中,如果 _autoStyles 存在但样式未应用,需要重新应用
|
|
263
|
+
const stylesToApply = this._autoStyles || this.config.styles;
|
|
264
|
+
if (stylesToApply) {
|
|
265
|
+
const styleName = this.config.styleName || this.constructor.name;
|
|
266
|
+
StyleManager.applyStyles(this.shadowRoot, styleName, stylesToApply);
|
|
267
|
+
}
|
|
211
268
|
}
|
|
212
269
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
this.shadowRoot.appendChild(content);
|
|
270
|
+
// 8. 恢复焦点状态并清除渲染标志
|
|
271
|
+
this.restoreFocusState(focusState);
|
|
272
|
+
this._pendingFocusState = null;
|
|
217
273
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const oldChildren = Array.from(this.shadowRoot.children).filter((child) => {
|
|
221
|
-
// 保留新添加的内容
|
|
222
|
-
if (child === content) {
|
|
223
|
-
return false;
|
|
224
|
-
}
|
|
225
|
-
// 保留样式元素
|
|
226
|
-
if (child instanceof HTMLStyleElement) {
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
// 保留未标记的元素(第三方库注入的元素、自定义元素)
|
|
230
|
-
// 这是 RFC 0037 Phase 5 的核心:保护未标记元素
|
|
231
|
-
if (shouldPreserveElement(child)) {
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
return true;
|
|
235
|
-
});
|
|
236
|
-
oldChildren.forEach((child) => child.remove());
|
|
274
|
+
// 9. 调用 onRendered 生命周期钩子
|
|
275
|
+
this.onRendered?.();
|
|
237
276
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
this.restoreFocusState(focusState);
|
|
241
|
-
this._pendingFocusState = null;
|
|
242
|
-
// 调用 onRendered 生命周期钩子
|
|
243
|
-
this.onRendered?.();
|
|
244
|
-
// 在 onRendered() 完成后清除渲染标志,允许后续的 scheduleRerender()
|
|
245
|
-
this._isRendering = false;
|
|
246
|
-
});
|
|
247
|
-
});
|
|
277
|
+
// 10. 清除渲染标志,允许后续的 scheduleRerender()
|
|
278
|
+
this._isRendering = false;
|
|
248
279
|
} catch (error) {
|
|
249
280
|
logger.error("Error in _rerender:", error);
|
|
250
281
|
this.renderError(error);
|