@wsxjs/wsx-core 0.0.20 → 0.0.22
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 +1091 -0
- package/dist/index.js +869 -113
- package/dist/index.mjs +155 -22
- package/dist/jsx-runtime.js +723 -97
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +723 -97
- package/dist/jsx.mjs +1 -1
- package/package.json +2 -2
- package/src/base-component.ts +15 -0
- package/src/dom-cache-manager.ts +135 -0
- package/src/jsx-factory.ts +133 -447
- package/src/light-component.ts +12 -4
- package/src/reactive-decorator.ts +9 -0
- package/src/render-context.ts +40 -0
- package/src/utils/cache-key.ts +114 -0
- package/src/utils/dom-utils.ts +119 -0
- package/src/utils/element-creation.ts +140 -0
- package/src/utils/element-marking.ts +80 -0
- package/src/utils/element-update.ts +633 -0
- package/src/utils/props-utils.ts +307 -0
- package/src/web-component.ts +24 -6
- package/dist/chunk-7FXISNME.mjs +0 -462
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element Update Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for fine-grained DOM updates (RFC 0037 Phase 4).
|
|
5
|
+
* These functions update only changed props and children, avoiding full element recreation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { shouldUseSVGNamespace, getSVGAttributeName } from "./svg-utils";
|
|
9
|
+
import { flattenChildren, type JSXChildren } from "./dom-utils";
|
|
10
|
+
import { setSmartProperty, isFrameworkInternalProp } from "./props-utils";
|
|
11
|
+
import { shouldPreserveElement, getElementCacheKey } from "./element-marking";
|
|
12
|
+
import type { DOMCacheManager } from "../dom-cache-manager";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Removes a property from an element.
|
|
16
|
+
*/
|
|
17
|
+
function removeProp(
|
|
18
|
+
element: HTMLElement | SVGElement,
|
|
19
|
+
key: string,
|
|
20
|
+
oldValue: unknown,
|
|
21
|
+
tag: string
|
|
22
|
+
): void {
|
|
23
|
+
const isSVG = shouldUseSVGNamespace(tag);
|
|
24
|
+
|
|
25
|
+
// 处理特殊属性
|
|
26
|
+
if (key === "ref") {
|
|
27
|
+
// ref 是回调,不需要移除
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (key === "className" || key === "class") {
|
|
32
|
+
if (isSVG) {
|
|
33
|
+
element.removeAttribute("class");
|
|
34
|
+
} else {
|
|
35
|
+
(element as HTMLElement).className = "";
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (key === "style") {
|
|
41
|
+
element.removeAttribute("style");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (key.startsWith("on") && typeof oldValue === "function") {
|
|
46
|
+
// 事件监听器:需要移除(但无法获取原始监听器,所以跳过)
|
|
47
|
+
// 注意:这可能导致内存泄漏,但在实际使用中,事件监听器通常不会变化
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (key === "value") {
|
|
52
|
+
if (
|
|
53
|
+
element instanceof HTMLInputElement ||
|
|
54
|
+
element instanceof HTMLTextAreaElement ||
|
|
55
|
+
element instanceof HTMLSelectElement
|
|
56
|
+
) {
|
|
57
|
+
element.value = "";
|
|
58
|
+
} else {
|
|
59
|
+
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
60
|
+
element.removeAttribute(attributeName);
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 过滤框架内部属性(不应该从 DOM 移除,因为它们本来就不应该存在)
|
|
66
|
+
if (isFrameworkInternalProp(key)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 移除其他属性
|
|
71
|
+
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
72
|
+
element.removeAttribute(attributeName);
|
|
73
|
+
|
|
74
|
+
// 尝试移除 JavaScript 属性
|
|
75
|
+
try {
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
delete (element as any)[key];
|
|
78
|
+
} catch {
|
|
79
|
+
// 忽略删除失败
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Applies a single prop to an element (same logic as element-creation.ts).
|
|
85
|
+
*/
|
|
86
|
+
function applySingleProp(
|
|
87
|
+
element: HTMLElement | SVGElement,
|
|
88
|
+
key: string,
|
|
89
|
+
value: unknown,
|
|
90
|
+
tag: string,
|
|
91
|
+
isSVG: boolean
|
|
92
|
+
): void {
|
|
93
|
+
if (value === null || value === undefined || value === false) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 处理ref回调
|
|
98
|
+
if (key === "ref" && typeof value === "function") {
|
|
99
|
+
value(element);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 处理className和class
|
|
104
|
+
if (key === "className" || key === "class") {
|
|
105
|
+
if (isSVG) {
|
|
106
|
+
element.setAttribute("class", value as string);
|
|
107
|
+
} else {
|
|
108
|
+
(element as HTMLElement).className = value as string;
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 处理style
|
|
114
|
+
if (key === "style" && typeof value === "string") {
|
|
115
|
+
element.setAttribute("style", value);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 处理事件监听器
|
|
120
|
+
if (key.startsWith("on") && typeof value === "function") {
|
|
121
|
+
const eventName = key.slice(2).toLowerCase();
|
|
122
|
+
// 注意:这里会重复添加事件监听器,但这是预期的行为
|
|
123
|
+
// 在实际使用中,事件监听器通常不会频繁变化
|
|
124
|
+
element.addEventListener(eventName, value as EventListener);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 处理布尔属性
|
|
129
|
+
if (typeof value === "boolean") {
|
|
130
|
+
if (value) {
|
|
131
|
+
element.setAttribute(key, "");
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 特殊处理 input/textarea/select 的 value 属性
|
|
137
|
+
if (key === "value") {
|
|
138
|
+
if (
|
|
139
|
+
element instanceof HTMLInputElement ||
|
|
140
|
+
element instanceof HTMLTextAreaElement ||
|
|
141
|
+
element instanceof HTMLSelectElement
|
|
142
|
+
) {
|
|
143
|
+
element.value = String(value);
|
|
144
|
+
} else {
|
|
145
|
+
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
146
|
+
element.setAttribute(attributeName, String(value));
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 过滤框架内部属性(不应该渲染到 DOM)
|
|
152
|
+
if (isFrameworkInternalProp(key)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 处理其他属性 - 使用智能属性设置函数
|
|
157
|
+
setSmartProperty(element, key, value, tag);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Updates element props by comparing old and new props.
|
|
162
|
+
* Only updates changed properties.
|
|
163
|
+
*/
|
|
164
|
+
export function updateProps(
|
|
165
|
+
element: HTMLElement | SVGElement,
|
|
166
|
+
oldProps: Record<string, unknown> | null | undefined,
|
|
167
|
+
newProps: Record<string, unknown> | null | undefined,
|
|
168
|
+
tag: string
|
|
169
|
+
): void {
|
|
170
|
+
const isSVG = shouldUseSVGNamespace(tag);
|
|
171
|
+
const old = oldProps || {};
|
|
172
|
+
const new_ = newProps || {};
|
|
173
|
+
|
|
174
|
+
// 移除旧属性(在新 props 中不存在的)
|
|
175
|
+
for (const key in old) {
|
|
176
|
+
if (!(key in new_)) {
|
|
177
|
+
removeProp(element, key, old[key], tag);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 添加/更新新属性(值发生变化的)
|
|
182
|
+
// 优化:跳过相同值的属性,减少不必要的 DOM 操作
|
|
183
|
+
for (const key in new_) {
|
|
184
|
+
const oldValue = old[key];
|
|
185
|
+
const newValue = new_[key];
|
|
186
|
+
|
|
187
|
+
// 快速路径:引用相等,跳过
|
|
188
|
+
if (oldValue === newValue) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 如果 oldValue 是 undefined,说明是新增属性,需要设置
|
|
193
|
+
if (oldValue === undefined) {
|
|
194
|
+
applySingleProp(element, key, newValue, tag, isSVG);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 深度比较:对于对象和数组,使用 JSON.stringify 进行深度比较
|
|
199
|
+
// 注意:JSON.stringify 会进行完整的深度比较,不仅仅是第一层
|
|
200
|
+
// 关键修复:即使 JSON.stringify 结果相同,如果引用不同,也应该更新
|
|
201
|
+
// 因为对象可能包含函数、Symbol 等无法序列化的内容,或者对象引用变化意味着需要更新
|
|
202
|
+
if (
|
|
203
|
+
typeof oldValue === "object" &&
|
|
204
|
+
oldValue !== null &&
|
|
205
|
+
typeof newValue === "object" &&
|
|
206
|
+
newValue !== null
|
|
207
|
+
) {
|
|
208
|
+
// 深度比较:使用 JSON.stringify 比较整个对象结构
|
|
209
|
+
// 这样可以检测到嵌套对象(如 navigation.items[].label)的变化
|
|
210
|
+
try {
|
|
211
|
+
const oldJson = JSON.stringify(oldValue);
|
|
212
|
+
const newJson = JSON.stringify(newValue);
|
|
213
|
+
if (oldJson === newJson) {
|
|
214
|
+
// JSON 字符串相同,但引用不同
|
|
215
|
+
// 对于自定义元素的属性(如 navigation),即使 JSON 相同,引用变化也可能需要更新
|
|
216
|
+
// 因为 setter 可能会触发其他逻辑
|
|
217
|
+
// 但是,为了性能,我们只在 JSON 不同时才更新
|
|
218
|
+
// 如果 JSON 相同但引用不同,可能是同一个对象的不同引用,不需要更新
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
// 如果 JSON.stringify 失败(如循环引用),认为对象已变化
|
|
223
|
+
// 继续执行,更新属性
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 值确实变化了,更新属性
|
|
228
|
+
applySingleProp(element, key, newValue, tag, isSVG);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Updates element children by comparing old and new children.
|
|
234
|
+
* Simple version: only handles same number of children.
|
|
235
|
+
*/
|
|
236
|
+
export function updateChildren(
|
|
237
|
+
element: HTMLElement | SVGElement,
|
|
238
|
+
oldChildren: JSXChildren[],
|
|
239
|
+
newChildren: JSXChildren[]
|
|
240
|
+
): void {
|
|
241
|
+
const flatOld = flattenChildren(oldChildren);
|
|
242
|
+
const flatNew = flattenChildren(newChildren);
|
|
243
|
+
|
|
244
|
+
// 阶段 4 简化版:只处理相同数量的子节点
|
|
245
|
+
const minLength = Math.min(flatOld.length, flatNew.length);
|
|
246
|
+
|
|
247
|
+
// 更新现有子节点
|
|
248
|
+
// 关键:直接使用 oldChild 作为 oldNode(如果它是元素),因为它已经在 DOM 中
|
|
249
|
+
// 对于文本节点,按顺序匹配(跳过应该保留的元素节点)
|
|
250
|
+
let domIndex = 0; // DOM 中的实际索引,用于匹配文本节点
|
|
251
|
+
for (let i = 0; i < minLength; i++) {
|
|
252
|
+
const oldChild = flatOld[i];
|
|
253
|
+
const newChild = flatNew[i];
|
|
254
|
+
|
|
255
|
+
// 找到与 oldChild 对应的实际 DOM 节点
|
|
256
|
+
// 关键:oldChild 是上次渲染的元素引用,如果它在 DOM 中,直接使用它
|
|
257
|
+
let oldNode: Node | null = null;
|
|
258
|
+
if (oldChild instanceof HTMLElement || oldChild instanceof SVGElement) {
|
|
259
|
+
// 元素节点:检查 oldChild 是否在 DOM 中
|
|
260
|
+
// 如果 oldChild 的 parentNode 是当前 element,说明它在 DOM 中
|
|
261
|
+
if (oldChild.parentNode === element) {
|
|
262
|
+
// 关键修复:确保 oldChild 不是应该保留的元素(手动创建的元素、第三方库注入的元素)
|
|
263
|
+
// 如果 oldChild 是应该保留的元素,不应该在"更新现有子节点"循环中处理它
|
|
264
|
+
if (!shouldPreserveElement(oldChild)) {
|
|
265
|
+
oldNode = oldChild;
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
// oldChild 不在 DOM 中,尝试通过 cache key 找到对应的 DOM 节点
|
|
269
|
+
// 这可以处理元素被替换但 cache key 相同的情况
|
|
270
|
+
const oldCacheKey = getElementCacheKey(oldChild);
|
|
271
|
+
if (oldCacheKey) {
|
|
272
|
+
// 遍历 DOM 中的子节点,找到具有相同 cache key 的节点
|
|
273
|
+
for (let j = 0; j < element.childNodes.length; j++) {
|
|
274
|
+
const domChild = element.childNodes[j];
|
|
275
|
+
if (domChild instanceof HTMLElement || domChild instanceof SVGElement) {
|
|
276
|
+
// 跳过应该保留的元素(手动创建的元素、第三方库注入的元素)
|
|
277
|
+
if (shouldPreserveElement(domChild)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const domCacheKey = getElementCacheKey(domChild);
|
|
281
|
+
if (domCacheKey === oldCacheKey) {
|
|
282
|
+
oldNode = domChild;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// 如果 oldChild 不在 DOM 中且找不到对应的节点,oldNode 保持为 null
|
|
290
|
+
} else if (typeof oldChild === "string" || typeof oldChild === "number") {
|
|
291
|
+
// 文本节点:按顺序找到对应的文本节点(跳过所有元素节点)
|
|
292
|
+
// 关键:跳过应该保留的元素节点(手动创建的元素、第三方库注入的元素)
|
|
293
|
+
while (domIndex < element.childNodes.length) {
|
|
294
|
+
const node = element.childNodes[domIndex];
|
|
295
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
296
|
+
oldNode = node;
|
|
297
|
+
domIndex++;
|
|
298
|
+
break;
|
|
299
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
300
|
+
// 跳过所有元素节点(不管是保留的还是框架管理的)
|
|
301
|
+
// 因为元素节点会在自己的迭代中处理
|
|
302
|
+
// 注意:手动创建的元素会被 shouldPreserveElement 保护,不会被移除
|
|
303
|
+
domIndex++;
|
|
304
|
+
} else {
|
|
305
|
+
// 跳过其他类型的节点
|
|
306
|
+
domIndex++;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 如果是文本节点,更新文本内容
|
|
312
|
+
if (typeof oldChild === "string" || typeof oldChild === "number") {
|
|
313
|
+
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
314
|
+
const oldText = String(oldChild);
|
|
315
|
+
const newText = String(newChild);
|
|
316
|
+
|
|
317
|
+
// 关键修复:始终检查 DOM 中的实际文本内容,而不仅仅依赖 oldChild
|
|
318
|
+
// 这样可以确保即使元数据不同步,也能正确更新
|
|
319
|
+
const needsUpdate =
|
|
320
|
+
oldText !== newText ||
|
|
321
|
+
(oldNode &&
|
|
322
|
+
oldNode.nodeType === Node.TEXT_NODE &&
|
|
323
|
+
oldNode.textContent !== newText);
|
|
324
|
+
|
|
325
|
+
if (!needsUpdate) {
|
|
326
|
+
// 文本内容确实相同,跳过更新
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (oldNode && oldNode.nodeType === Node.TEXT_NODE) {
|
|
331
|
+
// 更新现有文本节点
|
|
332
|
+
oldNode.textContent = newText;
|
|
333
|
+
} else {
|
|
334
|
+
// 创建新的文本节点
|
|
335
|
+
const newTextNode = document.createTextNode(newText);
|
|
336
|
+
if (oldNode && !shouldPreserveElement(oldNode)) {
|
|
337
|
+
element.replaceChild(newTextNode, oldNode);
|
|
338
|
+
} else {
|
|
339
|
+
element.insertBefore(newTextNode, oldNode || null);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
// 类型变化:文本 -> 元素
|
|
344
|
+
if (oldNode && !shouldPreserveElement(oldNode)) {
|
|
345
|
+
element.removeChild(oldNode);
|
|
346
|
+
}
|
|
347
|
+
if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
348
|
+
if (newChild.parentNode !== element) {
|
|
349
|
+
element.insertBefore(newChild, oldNode || null);
|
|
350
|
+
}
|
|
351
|
+
} else if (newChild instanceof DocumentFragment) {
|
|
352
|
+
element.insertBefore(newChild, oldNode || null);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} else if (oldChild instanceof HTMLElement || oldChild instanceof SVGElement) {
|
|
356
|
+
// 关键修复:如果 oldNode 是应该保留的元素(手动创建的元素、第三方库注入的元素),跳过处理
|
|
357
|
+
// 这些元素不在 oldChildren 或 newChildren 中,应该在第二步被保留
|
|
358
|
+
if (oldNode && shouldPreserveElement(oldNode)) {
|
|
359
|
+
// 跳过应该保留的元素,继续处理下一个
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 如果是元素节点,检查是否是同一个元素
|
|
364
|
+
if (newChild === oldChild) {
|
|
365
|
+
// 同一个元素,不需要更新(元素内容会在 updateElement 中更新)
|
|
366
|
+
continue;
|
|
367
|
+
} else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
368
|
+
// 不同的元素引用,需要替换
|
|
369
|
+
// 检查 cache key:如果 cache key 相同,说明是同一个逻辑位置,应该替换
|
|
370
|
+
const oldCacheKey =
|
|
371
|
+
oldNode && (oldNode instanceof HTMLElement || oldNode instanceof SVGElement)
|
|
372
|
+
? getElementCacheKey(oldNode)
|
|
373
|
+
: null;
|
|
374
|
+
const newCacheKey = getElementCacheKey(newChild);
|
|
375
|
+
const hasSameCacheKey = oldCacheKey && newCacheKey && oldCacheKey === newCacheKey;
|
|
376
|
+
|
|
377
|
+
if (oldNode) {
|
|
378
|
+
// oldNode 存在(oldChild 在 DOM 中)
|
|
379
|
+
if (!shouldPreserveElement(oldNode)) {
|
|
380
|
+
// 可以替换
|
|
381
|
+
if (oldNode !== newChild) {
|
|
382
|
+
// 如果 newChild 已经在 DOM 中,需要先移除它(避免重复)
|
|
383
|
+
if (newChild.parentNode === element) {
|
|
384
|
+
// newChild 已经在当前 element 中
|
|
385
|
+
// 如果 cache key 相同,说明是同一个逻辑位置,应该替换 oldNode
|
|
386
|
+
if (hasSameCacheKey) {
|
|
387
|
+
// 如果 newChild 就是 oldNode,不需要替换
|
|
388
|
+
if (newChild !== oldNode) {
|
|
389
|
+
// 替换旧元素(replaceChild 会自动从 newChild 的旧位置移除它)
|
|
390
|
+
// 但是,如果 newChild 在 oldNode 之后,replaceChild 会先移除 newChild,然后替换 oldNode
|
|
391
|
+
// 这会导致 newChild 移动到 oldNode 的位置,这是正确的
|
|
392
|
+
// 如果 newChild 在 oldNode 之前,replaceChild 也会将 newChild 移动到 oldNode 的位置
|
|
393
|
+
// 所以,无论 newChild 在哪里,replaceChild 都会将其移动到 oldNode 的位置
|
|
394
|
+
element.replaceChild(newChild, oldNode);
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
// cache key 不同,说明 newChild 在错误的位置
|
|
398
|
+
// 先移除 newChild(它可能在错误的位置)
|
|
399
|
+
element.removeChild(newChild);
|
|
400
|
+
// 然后替换 oldNode
|
|
401
|
+
element.replaceChild(newChild, oldNode);
|
|
402
|
+
}
|
|
403
|
+
} else if (newChild.parentNode) {
|
|
404
|
+
// newChild 在其他父元素中,先移除
|
|
405
|
+
newChild.parentNode.removeChild(newChild);
|
|
406
|
+
// 然后替换 oldNode
|
|
407
|
+
element.replaceChild(newChild, oldNode);
|
|
408
|
+
} else {
|
|
409
|
+
// newChild 不在 DOM 中,直接替换
|
|
410
|
+
element.replaceChild(newChild, oldNode);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
// 应该保留旧节点,只添加新节点(不替换)
|
|
415
|
+
if (newChild.parentNode !== element) {
|
|
416
|
+
// 如果 newChild 在其他父元素中,先移除
|
|
417
|
+
if (newChild.parentNode) {
|
|
418
|
+
newChild.parentNode.removeChild(newChild);
|
|
419
|
+
}
|
|
420
|
+
element.insertBefore(newChild, oldNode.nextSibling);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
// oldNode 不存在(oldChild 不在 DOM 中),直接添加新元素
|
|
425
|
+
if (newChild.parentNode !== element) {
|
|
426
|
+
// 如果 newChild 在其他父元素中,先移除
|
|
427
|
+
if (newChild.parentNode) {
|
|
428
|
+
newChild.parentNode.removeChild(newChild);
|
|
429
|
+
}
|
|
430
|
+
element.appendChild(newChild);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
// 类型变化:元素 -> 文本
|
|
435
|
+
if (oldNode && !shouldPreserveElement(oldNode)) {
|
|
436
|
+
element.removeChild(oldNode);
|
|
437
|
+
}
|
|
438
|
+
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
439
|
+
const newTextNode = document.createTextNode(String(newChild));
|
|
440
|
+
element.insertBefore(newTextNode, oldNode?.nextSibling || null);
|
|
441
|
+
} else if (newChild instanceof DocumentFragment) {
|
|
442
|
+
element.insertBefore(newChild, oldNode?.nextSibling || null);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 添加新子节点
|
|
449
|
+
for (let i = minLength; i < flatNew.length; i++) {
|
|
450
|
+
const newChild = flatNew[i];
|
|
451
|
+
if (newChild === null || newChild === undefined || newChild === false) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
456
|
+
element.appendChild(document.createTextNode(String(newChild)));
|
|
457
|
+
} else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
458
|
+
// 确保子元素正确添加到当前父容器
|
|
459
|
+
// 如果 newChild 已经在 DOM 中,需要检查它是否在正确的位置
|
|
460
|
+
if (newChild.parentNode === element) {
|
|
461
|
+
// newChild 已经在当前 element 中
|
|
462
|
+
// 检查它是否在正确的位置(应该在最后,因为这是"添加新子节点"部分)
|
|
463
|
+
const currentIndex = Array.from(element.childNodes).indexOf(newChild);
|
|
464
|
+
const expectedIndex = element.childNodes.length - 1;
|
|
465
|
+
if (currentIndex !== expectedIndex) {
|
|
466
|
+
// 位置不对,移动到正确位置(末尾)
|
|
467
|
+
element.removeChild(newChild);
|
|
468
|
+
element.appendChild(newChild);
|
|
469
|
+
}
|
|
470
|
+
// 如果位置正确,跳过(避免重复添加)
|
|
471
|
+
// 注意:元素内容会在 updateElement 中更新,所以这里只需要确保位置正确
|
|
472
|
+
continue;
|
|
473
|
+
} else if (newChild.parentNode) {
|
|
474
|
+
// newChild 在其他父元素中,先移除
|
|
475
|
+
newChild.parentNode.removeChild(newChild);
|
|
476
|
+
}
|
|
477
|
+
// 添加 newChild 到当前 element 的末尾
|
|
478
|
+
element.appendChild(newChild);
|
|
479
|
+
} else if (newChild instanceof DocumentFragment) {
|
|
480
|
+
element.appendChild(newChild);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 移除多余子节点(阶段 5:正确处理元素保留)
|
|
485
|
+
// 关键:需要跳过"应该保留"的元素(第三方库注入的元素)
|
|
486
|
+
// 以及已经在 newChildren 中的元素(通过元素引用或 cache key 匹配)
|
|
487
|
+
const nodesToRemove: Node[] = [];
|
|
488
|
+
const newChildSet = new Set<HTMLElement | SVGElement | DocumentFragment>();
|
|
489
|
+
const newChildCacheKeyMap = new Map<string, HTMLElement | SVGElement>();
|
|
490
|
+
// 只将元素节点添加到 Set 中(文本节点不能直接比较)
|
|
491
|
+
for (const child of flatNew) {
|
|
492
|
+
if (
|
|
493
|
+
child instanceof HTMLElement ||
|
|
494
|
+
child instanceof SVGElement ||
|
|
495
|
+
child instanceof DocumentFragment
|
|
496
|
+
) {
|
|
497
|
+
newChildSet.add(child);
|
|
498
|
+
// 同时收集 cache key 到 Map,用于匹配(即使元素引用不同,cache key 相同也认为是同一个元素)
|
|
499
|
+
// 注意:DocumentFragment 没有 cache key,只能通过引用匹配
|
|
500
|
+
if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
501
|
+
const cacheKey = getElementCacheKey(child);
|
|
502
|
+
if (cacheKey) {
|
|
503
|
+
// 如果 cache key 已存在,保留最新的元素(newChild)
|
|
504
|
+
newChildCacheKeyMap.set(cacheKey, child);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// 第一步:处理重复的 cache key(如果 DOM 中有多个元素具有相同的 cache key,只保留 newChild)
|
|
511
|
+
// 注意:需要从后往前遍历,避免在循环中修改 DOM 导致索引问题
|
|
512
|
+
// 关键:这个步骤在"更新现有子节点"循环之后执行,所以需要处理那些在"更新现有子节点"循环中没有处理的重复元素
|
|
513
|
+
const processedCacheKeys = new Set<string>();
|
|
514
|
+
// 构建 newChild 到其在 flatNew 中索引的映射,用于确定正确位置
|
|
515
|
+
const newChildToIndexMap = new Map<HTMLElement | SVGElement, number>();
|
|
516
|
+
for (let i = 0; i < flatNew.length; i++) {
|
|
517
|
+
const child = flatNew[i];
|
|
518
|
+
if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
519
|
+
newChildToIndexMap.set(child, i);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
for (let i = element.childNodes.length - 1; i >= 0; i--) {
|
|
524
|
+
const child = element.childNodes[i];
|
|
525
|
+
if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
526
|
+
// 关键修复:跳过应该保留的元素(第三方库注入的元素)
|
|
527
|
+
if (shouldPreserveElement(child)) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const cacheKey = getElementCacheKey(child);
|
|
531
|
+
if (
|
|
532
|
+
cacheKey &&
|
|
533
|
+
newChildCacheKeyMap.has(cacheKey) &&
|
|
534
|
+
!processedCacheKeys.has(cacheKey)
|
|
535
|
+
) {
|
|
536
|
+
processedCacheKeys.add(cacheKey);
|
|
537
|
+
const newChild = newChildCacheKeyMap.get(cacheKey)!;
|
|
538
|
+
// 如果 child 不是 newChild,说明是旧元素,应该被移除或替换
|
|
539
|
+
if (child !== newChild) {
|
|
540
|
+
// 如果 newChild 已经在 DOM 中,移除旧元素
|
|
541
|
+
if (newChild.parentNode === element) {
|
|
542
|
+
// newChild 已经在 DOM 中,移除旧元素
|
|
543
|
+
// 注意:newChild 已经在 DOM 中,所以不需要再次添加
|
|
544
|
+
// 但需要确保 newChild 在正确的位置(通过 replaceChild 移动到旧元素的位置)
|
|
545
|
+
// 这样可以确保 newChild 在正确的位置,而不是在错误的位置
|
|
546
|
+
// 但是,如果 newChild 在 child 之后,replaceChild 会先移除 newChild,然后替换 child
|
|
547
|
+
// 这会导致 newChild 移动到 child 的位置,这是正确的
|
|
548
|
+
// 如果 newChild 在 child 之前,replaceChild 也会将 newChild 移动到 child 的位置
|
|
549
|
+
// 所以,无论 newChild 在哪里,replaceChild 都会将其移动到 child 的位置
|
|
550
|
+
element.replaceChild(newChild, child);
|
|
551
|
+
} else {
|
|
552
|
+
// newChild 不在 DOM 中,替换旧元素
|
|
553
|
+
element.replaceChild(newChild, child);
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
// child === newChild,说明是同一个元素,不需要处理
|
|
557
|
+
// 但需要确保它在正确的位置(这应该在"更新现有子节点"循环中处理)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// 第二步:移除不在 newChildren 中的元素
|
|
564
|
+
for (let i = 0; i < element.childNodes.length; i++) {
|
|
565
|
+
const child = element.childNodes[i];
|
|
566
|
+
|
|
567
|
+
// 跳过应该保留的元素(第三方库注入的元素)
|
|
568
|
+
if (shouldPreserveElement(child)) {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// 跳过已经在 newChildren 中的元素(通过元素引用或 cache key 匹配)
|
|
573
|
+
if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
574
|
+
// 方法 1: 直接元素引用匹配
|
|
575
|
+
if (newChildSet.has(child)) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
// 方法 2: cache key 匹配(用于处理语言切换等场景,元素引用可能不同但 cache key 相同)
|
|
579
|
+
const cacheKey = getElementCacheKey(child);
|
|
580
|
+
if (cacheKey && newChildCacheKeyMap.has(cacheKey)) {
|
|
581
|
+
// 已经在第一步处理过了,跳过
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
} else if (child instanceof DocumentFragment) {
|
|
585
|
+
// DocumentFragment 只能通过引用匹配
|
|
586
|
+
if (newChildSet.has(child)) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// 只有不应该保留且不在 newChildren 中的节点才添加到移除列表
|
|
592
|
+
nodesToRemove.push(child);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// 统一移除(从后往前移除,避免索引变化)
|
|
596
|
+
for (let i = nodesToRemove.length - 1; i >= 0; i--) {
|
|
597
|
+
const node = nodesToRemove[i];
|
|
598
|
+
if (node.parentNode === element) {
|
|
599
|
+
element.removeChild(node);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Updates an element with new props and children.
|
|
606
|
+
* Stores metadata in cache manager for next update.
|
|
607
|
+
*/
|
|
608
|
+
export function updateElement(
|
|
609
|
+
element: HTMLElement | SVGElement,
|
|
610
|
+
newProps: Record<string, unknown> | null,
|
|
611
|
+
newChildren: JSXChildren[],
|
|
612
|
+
tag: string,
|
|
613
|
+
cacheManager: DOMCacheManager
|
|
614
|
+
): void {
|
|
615
|
+
// 获取旧的元数据
|
|
616
|
+
const oldMetadata = cacheManager.getMetadata(element);
|
|
617
|
+
const oldProps = (oldMetadata?.props as Record<string, unknown>) || null;
|
|
618
|
+
const oldChildren = (oldMetadata?.children as JSXChildren[]) || [];
|
|
619
|
+
|
|
620
|
+
// 关键修复:在更新 DOM 之前先保存新的元数据
|
|
621
|
+
// 这样可以防止竞态条件:如果在更新过程中触发了另一个渲染,
|
|
622
|
+
// 新渲染会读取到正确的元数据,而不是过时的数据
|
|
623
|
+
cacheManager.setMetadata(element, {
|
|
624
|
+
props: newProps || {},
|
|
625
|
+
children: newChildren,
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// 更新 props
|
|
629
|
+
updateProps(element, oldProps, newProps, tag);
|
|
630
|
+
|
|
631
|
+
// 更新 children
|
|
632
|
+
updateChildren(element, oldChildren, newChildren);
|
|
633
|
+
}
|