@wsxjs/wsx-core 0.0.21 → 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-AR3DIDLV.mjs → chunk-OXFZ575O.mjs} +223 -38
- package/dist/chunk-TKHKPLBM.mjs +1142 -0
- package/dist/index.js +318 -81
- package/dist/index.mjs +19 -6
- package/dist/jsx-runtime.js +302 -77
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +302 -77
- package/dist/jsx.mjs +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +2 -2
- package/src/jsx-factory.ts +66 -2
- package/src/light-component.ts +9 -2
- package/src/utils/element-update.ts +172 -110
- package/src/utils/update-children-helpers.ts +342 -0
- package/src/web-component.ts +22 -5
package/src/jsx-factory.ts
CHANGED
|
@@ -56,7 +56,44 @@ export function h(
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// 无上下文:使用旧逻辑(向后兼容)
|
|
59
|
-
|
|
59
|
+
// 关键修复:即使没有上下文,也要标记元素,以便框架能够正确管理它
|
|
60
|
+
// 否则,未标记的元素会被 shouldPreserveElement() 保留,导致重复元素
|
|
61
|
+
// 调试日志:记录上下文丢失的情况,帮助定位问题(仅在开发环境输出)
|
|
62
|
+
try {
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
const nodeEnv = (typeof (globalThis as any).process !== "undefined" &&
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
+
(globalThis as any).process.env?.NODE_ENV) as string | undefined;
|
|
67
|
+
if (nodeEnv === "development") {
|
|
68
|
+
if (!context) {
|
|
69
|
+
logger.debug(
|
|
70
|
+
`h() called without render context. Tag: "${tag}", ComponentId: "${getComponentId()}"`,
|
|
71
|
+
{
|
|
72
|
+
tag,
|
|
73
|
+
props: props ? Object.keys(props) : [],
|
|
74
|
+
hasCacheManager: !!cacheManager,
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
} else if (!cacheManager) {
|
|
78
|
+
logger.debug(
|
|
79
|
+
`h() called with context but no cache manager. Tag: "${tag}", Component: "${context.constructor.name}"`,
|
|
80
|
+
{
|
|
81
|
+
tag,
|
|
82
|
+
component: context.constructor.name,
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// 忽略环境变量检查错误
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const element = createElementWithPropsAndChildren(tag, props, children);
|
|
92
|
+
// 生成一个简单的 cache key(即使没有上下文)
|
|
93
|
+
const componentId = getComponentId();
|
|
94
|
+
const cacheKey = generateCacheKey(tag, props, componentId, context || undefined);
|
|
95
|
+
markElement(element, cacheKey);
|
|
96
|
+
return element;
|
|
60
97
|
}
|
|
61
98
|
|
|
62
99
|
/**
|
|
@@ -79,7 +116,28 @@ function tryUseCacheOrCreate(
|
|
|
79
116
|
// 缓存键已经确保了唯一性(componentId + tag + position/key/index)
|
|
80
117
|
// 不需要再检查标签名(可能导致错误复用)
|
|
81
118
|
const element = cachedElement as HTMLElement | SVGElement;
|
|
119
|
+
|
|
82
120
|
updateElement(element, props, children, tag, cacheManager);
|
|
121
|
+
|
|
122
|
+
// 关键修复(RFC-0039):检测自定义元素(Web Components)并重新触发生命周期
|
|
123
|
+
// 自定义元素有 connectedCallback 和 disconnectedCallback 方法
|
|
124
|
+
// 当它们被缓存复用时,需要模拟断开/重连以触发初始化逻辑
|
|
125
|
+
// 这确保了即使组件缺少 super.onConnected() 调用,框架层面也能保证正确的生命周期
|
|
126
|
+
const isCustomElement = tag.includes("-") && customElements.get(tag);
|
|
127
|
+
if (isCustomElement && element.isConnected) {
|
|
128
|
+
// 临时从 DOM 断开以触发 disconnectedCallback
|
|
129
|
+
const parent = element.parentNode;
|
|
130
|
+
if (parent) {
|
|
131
|
+
parent.removeChild(element);
|
|
132
|
+
// disconnectedCallback 会在 removeChild 时自动调用
|
|
133
|
+
|
|
134
|
+
// 立即重新添加以触发 connectedCallback
|
|
135
|
+
// 这确保生命周期在返回元素之前就已经完成
|
|
136
|
+
parent.appendChild(element);
|
|
137
|
+
// connectedCallback 会在 appendChild 时自动调用
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
83
141
|
return element;
|
|
84
142
|
}
|
|
85
143
|
|
|
@@ -120,7 +178,13 @@ function handleCacheError(
|
|
|
120
178
|
} catch {
|
|
121
179
|
// 忽略环境变量检查错误
|
|
122
180
|
}
|
|
123
|
-
|
|
181
|
+
// 关键修复:即使缓存失败,也要标记元素,以便框架能够正确管理它
|
|
182
|
+
const element = createElementWithPropsAndChildren(tag, props, children);
|
|
183
|
+
const context = RenderContext.getCurrentComponent();
|
|
184
|
+
const componentId = getComponentId();
|
|
185
|
+
const cacheKey = generateCacheKey(tag, props, componentId, context || undefined);
|
|
186
|
+
markElement(element, cacheKey);
|
|
187
|
+
return element;
|
|
124
188
|
}
|
|
125
189
|
|
|
126
190
|
/**
|
package/src/light-component.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { h, type JSXChildren } from "./jsx-factory";
|
|
11
11
|
import { BaseComponent, type BaseComponentConfig } from "./base-component";
|
|
12
12
|
import { RenderContext } from "./render-context";
|
|
13
|
+
import { shouldPreserveElement } from "./utils/element-marking";
|
|
13
14
|
import { createLogger } from "@wsxjs/wsx-logger";
|
|
14
15
|
|
|
15
16
|
const logger = createLogger("LightComponent");
|
|
@@ -222,7 +223,8 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
222
223
|
// 先添加新内容
|
|
223
224
|
this.appendChild(content);
|
|
224
225
|
|
|
225
|
-
// 移除旧内容(保留 JSX children
|
|
226
|
+
// 移除旧内容(保留 JSX children、样式元素和未标记元素)
|
|
227
|
+
// 关键修复:使用 shouldPreserveElement() 来保护手动创建的元素(如第三方库注入的元素)
|
|
226
228
|
const oldChildren = Array.from(this.children).filter((child) => {
|
|
227
229
|
// 保留新添加的内容
|
|
228
230
|
if (child === content) {
|
|
@@ -236,10 +238,15 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
236
238
|
) {
|
|
237
239
|
return false;
|
|
238
240
|
}
|
|
239
|
-
// 保留 JSX children
|
|
241
|
+
// 保留 JSX children(通过 JSX factory 直接添加的 children)
|
|
240
242
|
if (child instanceof HTMLElement && jsxChildren.includes(child)) {
|
|
241
243
|
return false;
|
|
242
244
|
}
|
|
245
|
+
// 保留未标记的元素(手动创建的元素、第三方库注入的元素)
|
|
246
|
+
// 这是 RFC 0037 Phase 5 的核心:保护未标记元素
|
|
247
|
+
if (shouldPreserveElement(child)) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
243
250
|
return true;
|
|
244
251
|
});
|
|
245
252
|
oldChildren.forEach((child) => child.remove());
|
|
@@ -6,10 +6,25 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { shouldUseSVGNamespace, getSVGAttributeName } from "./svg-utils";
|
|
9
|
-
import {
|
|
9
|
+
import { type JSXChildren } from "./dom-utils";
|
|
10
10
|
import { setSmartProperty, isFrameworkInternalProp } from "./props-utils";
|
|
11
11
|
import { shouldPreserveElement } from "./element-marking";
|
|
12
12
|
import type { DOMCacheManager } from "../dom-cache-manager";
|
|
13
|
+
import {
|
|
14
|
+
collectPreservedElements,
|
|
15
|
+
findElementNode,
|
|
16
|
+
findTextNode,
|
|
17
|
+
updateOrCreateTextNode,
|
|
18
|
+
removeNodeIfNotPreserved,
|
|
19
|
+
replaceOrInsertElement,
|
|
20
|
+
appendNewChild,
|
|
21
|
+
buildNewChildrenMaps,
|
|
22
|
+
deduplicateCacheKeys,
|
|
23
|
+
collectNodesToRemove,
|
|
24
|
+
removeNodes,
|
|
25
|
+
reinsertPreservedElements,
|
|
26
|
+
flattenChildrenSafe,
|
|
27
|
+
} from "./update-children-helpers";
|
|
13
28
|
|
|
14
29
|
/**
|
|
15
30
|
* Removes a property from an element.
|
|
@@ -189,16 +204,38 @@ export function updateProps(
|
|
|
189
204
|
continue;
|
|
190
205
|
}
|
|
191
206
|
|
|
192
|
-
//
|
|
207
|
+
// 如果 oldValue 是 undefined,说明是新增属性,需要设置
|
|
208
|
+
if (oldValue === undefined) {
|
|
209
|
+
applySingleProp(element, key, newValue, tag, isSVG);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 深度比较:对于对象和数组,使用 JSON.stringify 进行深度比较
|
|
214
|
+
// 注意:JSON.stringify 会进行完整的深度比较,不仅仅是第一层
|
|
215
|
+
// 关键修复:即使 JSON.stringify 结果相同,如果引用不同,也应该更新
|
|
216
|
+
// 因为对象可能包含函数、Symbol 等无法序列化的内容,或者对象引用变化意味着需要更新
|
|
193
217
|
if (
|
|
194
218
|
typeof oldValue === "object" &&
|
|
195
219
|
oldValue !== null &&
|
|
196
220
|
typeof newValue === "object" &&
|
|
197
221
|
newValue !== null
|
|
198
222
|
) {
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
223
|
+
// 深度比较:使用 JSON.stringify 比较整个对象结构
|
|
224
|
+
// 这样可以检测到嵌套对象(如 navigation.items[].label)的变化
|
|
225
|
+
try {
|
|
226
|
+
const oldJson = JSON.stringify(oldValue);
|
|
227
|
+
const newJson = JSON.stringify(newValue);
|
|
228
|
+
if (oldJson === newJson) {
|
|
229
|
+
// JSON 字符串相同,但引用不同
|
|
230
|
+
// 对于自定义元素的属性(如 navigation),即使 JSON 相同,引用变化也可能需要更新
|
|
231
|
+
// 因为 setter 可能会触发其他逻辑
|
|
232
|
+
// 但是,为了性能,我们只在 JSON 不同时才更新
|
|
233
|
+
// 如果 JSON 相同但引用不同,可能是同一个对象的不同引用,不需要更新
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// 如果 JSON.stringify 失败(如循环引用),认为对象已变化
|
|
238
|
+
// 继续执行,更新属性
|
|
202
239
|
}
|
|
203
240
|
}
|
|
204
241
|
|
|
@@ -214,92 +251,134 @@ export function updateProps(
|
|
|
214
251
|
export function updateChildren(
|
|
215
252
|
element: HTMLElement | SVGElement,
|
|
216
253
|
oldChildren: JSXChildren[],
|
|
217
|
-
newChildren: JSXChildren[]
|
|
254
|
+
newChildren: JSXChildren[],
|
|
255
|
+
cacheManager?: DOMCacheManager
|
|
218
256
|
): void {
|
|
219
|
-
const flatOld =
|
|
220
|
-
const flatNew =
|
|
257
|
+
const flatOld = flattenChildrenSafe(oldChildren);
|
|
258
|
+
const flatNew = flattenChildrenSafe(newChildren);
|
|
221
259
|
|
|
222
|
-
//
|
|
223
|
-
const
|
|
260
|
+
// 收集需要保留的元素(第三方库注入的元素)
|
|
261
|
+
const preservedElements = collectPreservedElements(element);
|
|
224
262
|
|
|
225
263
|
// 更新现有子节点
|
|
264
|
+
const minLength = Math.min(flatOld.length, flatNew.length);
|
|
265
|
+
const domIndex = { value: 0 }; // 使用对象包装,使其可在函数间传递
|
|
266
|
+
|
|
226
267
|
for (let i = 0; i < minLength; i++) {
|
|
227
268
|
const oldChild = flatOld[i];
|
|
228
269
|
const newChild = flatNew[i];
|
|
229
270
|
|
|
230
|
-
//
|
|
271
|
+
// 查找对应的 DOM 节点
|
|
272
|
+
let oldNode: Node | null = null;
|
|
273
|
+
if (oldChild instanceof HTMLElement || oldChild instanceof SVGElement) {
|
|
274
|
+
oldNode = findElementNode(oldChild, element);
|
|
275
|
+
// 关键修复:当处理元素节点时,需要更新 domIndex 以跳过该元素
|
|
276
|
+
// 这样,下一个文本节点的查找位置才是正确的
|
|
277
|
+
if (oldNode && oldNode.parentNode === element) {
|
|
278
|
+
// 找到 oldNode 在 DOM 中的位置
|
|
279
|
+
const nodeIndex = Array.from(element.childNodes).indexOf(oldNode as ChildNode);
|
|
280
|
+
if (nodeIndex !== -1 && nodeIndex >= domIndex.value) {
|
|
281
|
+
// 更新 domIndex 到 oldNode 之后的位置
|
|
282
|
+
domIndex.value = nodeIndex + 1;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} else if (typeof oldChild === "string" || typeof oldChild === "number") {
|
|
286
|
+
oldNode = findTextNode(element, domIndex);
|
|
287
|
+
// 关键修复:如果 findTextNode 返回 null,尝试从当前 domIndex 位置开始查找文本节点
|
|
288
|
+
// 这可以处理文本节点存在但 domIndex 不正确的情况
|
|
289
|
+
// Bug 1 修复:从 domIndex.value 开始搜索,而不是从 0 开始,避免重新处理已处理的节点
|
|
290
|
+
if (!oldNode && element.childNodes.length > 0) {
|
|
291
|
+
for (let j = domIndex.value; j < element.childNodes.length; j++) {
|
|
292
|
+
const node = element.childNodes[j];
|
|
293
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
294
|
+
oldNode = node;
|
|
295
|
+
// 更新 domIndex 到找到的文本节点之后
|
|
296
|
+
domIndex.value = j + 1;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 处理文本节点(oldChild 是字符串/数字)
|
|
231
304
|
if (typeof oldChild === "string" || typeof oldChild === "number") {
|
|
232
305
|
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
306
|
+
const oldText = String(oldChild);
|
|
307
|
+
const newText = String(newChild);
|
|
308
|
+
|
|
309
|
+
// Bug 2 修复:只有当文本内容确实需要更新时才调用 updateOrCreateTextNode
|
|
310
|
+
// 如果 oldText === newText 且 oldNode 为 null,说明文本节点可能已经存在且内容正确
|
|
311
|
+
// 或者不需要创建,因此不应该调用 updateOrCreateTextNode
|
|
312
|
+
const needsUpdate =
|
|
313
|
+
oldText !== newText ||
|
|
314
|
+
(oldNode &&
|
|
315
|
+
oldNode.nodeType === Node.TEXT_NODE &&
|
|
316
|
+
oldNode.textContent !== newText);
|
|
317
|
+
|
|
318
|
+
if (needsUpdate) {
|
|
319
|
+
updateOrCreateTextNode(element, oldNode, newText);
|
|
244
320
|
}
|
|
321
|
+
// 如果文本内容相同且 oldNode 为 null,不需要做任何操作
|
|
322
|
+
// 因为文本节点可能已经存在于 DOM 中且内容正确,或者不需要创建
|
|
245
323
|
} else {
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
if (!shouldPreserveElement(textNode)) {
|
|
251
|
-
element.removeChild(textNode);
|
|
252
|
-
}
|
|
253
|
-
// 如果应该保留,不删除(但可能仍需要添加新节点)
|
|
254
|
-
}
|
|
255
|
-
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
256
|
-
element.appendChild(document.createTextNode(String(newChild)));
|
|
257
|
-
} else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
258
|
-
element.appendChild(newChild);
|
|
324
|
+
// 类型变化:文本 -> 元素/Fragment
|
|
325
|
+
removeNodeIfNotPreserved(element, oldNode);
|
|
326
|
+
if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
327
|
+
replaceOrInsertElement(element, newChild, oldNode);
|
|
259
328
|
} else if (newChild instanceof DocumentFragment) {
|
|
260
|
-
element.
|
|
329
|
+
element.insertBefore(newChild, oldNode || null);
|
|
261
330
|
}
|
|
262
331
|
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
332
|
+
}
|
|
333
|
+
// 处理元素节点(oldChild 是元素)
|
|
334
|
+
else if (oldChild instanceof HTMLElement || oldChild instanceof SVGElement) {
|
|
335
|
+
if (oldNode && shouldPreserveElement(oldNode)) {
|
|
336
|
+
continue; // 跳过保留的元素
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 关键修复:即使 newChild === oldChild,如果它是元素,h() 应该已经调用了 updateElement 来更新其子元素
|
|
340
|
+
// 但是,为了确保子元素确实被更新了,我们不应该跳过,而是让后续代码确保元素在正确位置
|
|
341
|
+
// 如果 h() 已经正确更新了子元素,那么这里只需要确保元素在正确位置即可
|
|
342
|
+
if (
|
|
343
|
+
newChild === oldChild &&
|
|
344
|
+
(newChild instanceof HTMLElement || newChild instanceof SVGElement)
|
|
345
|
+
) {
|
|
346
|
+
// 同一个元素引用,h() 应该已经通过 updateElement 更新了其子元素
|
|
347
|
+
// 但是,如果 cacheManager 可用,我们可以验证并确保子元素确实被更新了
|
|
348
|
+
if (cacheManager) {
|
|
349
|
+
const childMetadata = cacheManager.getMetadata(newChild);
|
|
350
|
+
if (childMetadata) {
|
|
351
|
+
// 如果元数据存在,说明 h() 已经更新了子元素
|
|
352
|
+
// 只需要确保元素在正确位置
|
|
353
|
+
if (oldNode === newChild && newChild.parentNode === element) {
|
|
354
|
+
// 元素已经在正确位置,且 h() 应该已经更新了其子元素
|
|
355
|
+
// 不需要额外处理
|
|
356
|
+
continue;
|
|
282
357
|
}
|
|
283
358
|
}
|
|
284
359
|
} else {
|
|
285
|
-
|
|
286
|
-
|
|
360
|
+
// 如果没有 cacheManager,假设 h() 已经更新了子元素
|
|
361
|
+
if (oldNode === newChild && newChild.parentNode === element) {
|
|
362
|
+
continue;
|
|
287
363
|
}
|
|
288
364
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
// 如果应该保留,不删除(但可能仍需要添加新节点)
|
|
365
|
+
// 如果位置不对,需要调整
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
369
|
+
// 如果 newChild 已经在 DOM 中且位置正确,不需要替换
|
|
370
|
+
if (newChild.parentNode === element && oldNode === newChild) {
|
|
371
|
+
continue; // 元素已经在正确位置,不需要更新
|
|
298
372
|
}
|
|
373
|
+
replaceOrInsertElement(element, newChild, oldNode);
|
|
374
|
+
} else {
|
|
375
|
+
// 类型变化:元素 -> 文本/Fragment
|
|
376
|
+
removeNodeIfNotPreserved(element, oldNode);
|
|
299
377
|
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
300
|
-
|
|
378
|
+
const newTextNode = document.createTextNode(String(newChild));
|
|
379
|
+
element.insertBefore(newTextNode, oldNode?.nextSibling || null);
|
|
301
380
|
} else if (newChild instanceof DocumentFragment) {
|
|
302
|
-
element.
|
|
381
|
+
element.insertBefore(newChild, oldNode?.nextSibling || null);
|
|
303
382
|
}
|
|
304
383
|
}
|
|
305
384
|
}
|
|
@@ -307,44 +386,25 @@ export function updateChildren(
|
|
|
307
386
|
|
|
308
387
|
// 添加新子节点
|
|
309
388
|
for (let i = minLength; i < flatNew.length; i++) {
|
|
310
|
-
|
|
311
|
-
if (newChild === null || newChild === undefined || newChild === false) {
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
316
|
-
element.appendChild(document.createTextNode(String(newChild)));
|
|
317
|
-
} else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
318
|
-
// 确保子元素正确添加到当前父容器
|
|
319
|
-
// appendChild 会自动从旧父容器移除并添加到新父容器
|
|
320
|
-
// 但我们需要确保不重复添加已存在的子元素
|
|
321
|
-
if (newChild.parentNode !== element) {
|
|
322
|
-
element.appendChild(newChild);
|
|
323
|
-
}
|
|
324
|
-
} else if (newChild instanceof DocumentFragment) {
|
|
325
|
-
element.appendChild(newChild);
|
|
326
|
-
}
|
|
389
|
+
appendNewChild(element, flatNew[i]);
|
|
327
390
|
}
|
|
328
391
|
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
const
|
|
332
|
-
for (let i = flatNew.length; i < element.childNodes.length; i++) {
|
|
333
|
-
const child = element.childNodes[i];
|
|
334
|
-
if (!shouldPreserveElement(child)) {
|
|
335
|
-
// 只有不应该保留的节点才添加到移除列表
|
|
336
|
-
nodesToRemove.push(child);
|
|
337
|
-
}
|
|
338
|
-
// 如果应该保留,跳过(不添加到移除列表)
|
|
339
|
-
}
|
|
392
|
+
// 移除多余子节点(使用纯函数简化逻辑)
|
|
393
|
+
// 步骤 1: 构建新子元素的引用集合和 cache key 映射
|
|
394
|
+
const { elementSet, cacheKeyMap } = buildNewChildrenMaps(flatNew);
|
|
340
395
|
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
396
|
+
// 步骤 2: 处理重复的 cache key(确保每个 cache key 在 DOM 中只出现一次)
|
|
397
|
+
deduplicateCacheKeys(element, cacheKeyMap);
|
|
398
|
+
|
|
399
|
+
// 步骤 3: 收集需要移除的节点(跳过保留元素和新子元素)
|
|
400
|
+
const nodesToRemove = collectNodesToRemove(element, elementSet, cacheKeyMap);
|
|
401
|
+
|
|
402
|
+
// 步骤 4: 批量移除节点(从后往前,避免索引变化)
|
|
403
|
+
removeNodes(element, nodesToRemove);
|
|
404
|
+
|
|
405
|
+
// 步骤 5: 重新插入所有保留的元素到 DOM 末尾
|
|
406
|
+
// 这确保了第三方库注入的元素不会丢失
|
|
407
|
+
reinsertPreservedElements(element, preservedElements);
|
|
348
408
|
}
|
|
349
409
|
|
|
350
410
|
/**
|
|
@@ -363,15 +423,17 @@ export function updateElement(
|
|
|
363
423
|
const oldProps = (oldMetadata?.props as Record<string, unknown>) || null;
|
|
364
424
|
const oldChildren = (oldMetadata?.children as JSXChildren[]) || [];
|
|
365
425
|
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
// 更新 children
|
|
370
|
-
updateChildren(element, oldChildren, newChildren);
|
|
371
|
-
|
|
372
|
-
// 保存新的元数据
|
|
426
|
+
// 关键修复:在更新 DOM 之前先保存新的元数据
|
|
427
|
+
// 这样可以防止竞态条件:如果在更新过程中触发了另一个渲染,
|
|
428
|
+
// 新渲染会读取到正确的元数据,而不是过时的数据
|
|
373
429
|
cacheManager.setMetadata(element, {
|
|
374
430
|
props: newProps || {},
|
|
375
431
|
children: newChildren,
|
|
376
432
|
});
|
|
433
|
+
|
|
434
|
+
// 更新 props
|
|
435
|
+
updateProps(element, oldProps, newProps, tag);
|
|
436
|
+
|
|
437
|
+
// 更新 children
|
|
438
|
+
updateChildren(element, oldChildren, newChildren, cacheManager);
|
|
377
439
|
}
|