@wsxjs/wsx-core 0.0.21 → 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-AR3DIDLV.mjs → chunk-OXFZ575O.mjs} +223 -38
- package/dist/index.js +238 -42
- package/dist/index.mjs +19 -6
- package/dist/jsx-runtime.js +222 -38
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +222 -38
- package/dist/jsx.mjs +1 -1
- 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 +314 -58
- package/src/web-component.ts +22 -5
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { shouldUseSVGNamespace, getSVGAttributeName } from "./svg-utils";
|
|
9
9
|
import { flattenChildren, type JSXChildren } from "./dom-utils";
|
|
10
10
|
import { setSmartProperty, isFrameworkInternalProp } from "./props-utils";
|
|
11
|
-
import { shouldPreserveElement } from "./element-marking";
|
|
11
|
+
import { shouldPreserveElement, getElementCacheKey } from "./element-marking";
|
|
12
12
|
import type { DOMCacheManager } from "../dom-cache-manager";
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -189,16 +189,38 @@ export function updateProps(
|
|
|
189
189
|
continue;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
//
|
|
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 等无法序列化的内容,或者对象引用变化意味着需要更新
|
|
193
202
|
if (
|
|
194
203
|
typeof oldValue === "object" &&
|
|
195
204
|
oldValue !== null &&
|
|
196
205
|
typeof newValue === "object" &&
|
|
197
206
|
newValue !== null
|
|
198
207
|
) {
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
// 继续执行,更新属性
|
|
202
224
|
}
|
|
203
225
|
}
|
|
204
226
|
|
|
@@ -223,83 +245,201 @@ export function updateChildren(
|
|
|
223
245
|
const minLength = Math.min(flatOld.length, flatNew.length);
|
|
224
246
|
|
|
225
247
|
// 更新现有子节点
|
|
248
|
+
// 关键:直接使用 oldChild 作为 oldNode(如果它是元素),因为它已经在 DOM 中
|
|
249
|
+
// 对于文本节点,按顺序匹配(跳过应该保留的元素节点)
|
|
250
|
+
let domIndex = 0; // DOM 中的实际索引,用于匹配文本节点
|
|
226
251
|
for (let i = 0; i < minLength; i++) {
|
|
227
252
|
const oldChild = flatOld[i];
|
|
228
253
|
const newChild = flatNew[i];
|
|
229
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
|
+
|
|
230
311
|
// 如果是文本节点,更新文本内容
|
|
231
312
|
if (typeof oldChild === "string" || typeof oldChild === "number") {
|
|
232
313
|
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
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;
|
|
236
333
|
} else {
|
|
237
|
-
//
|
|
238
|
-
const newTextNode = document.createTextNode(
|
|
239
|
-
if (
|
|
240
|
-
element.replaceChild(newTextNode,
|
|
334
|
+
// 创建新的文本节点
|
|
335
|
+
const newTextNode = document.createTextNode(newText);
|
|
336
|
+
if (oldNode && !shouldPreserveElement(oldNode)) {
|
|
337
|
+
element.replaceChild(newTextNode, oldNode);
|
|
241
338
|
} else {
|
|
242
|
-
element.
|
|
339
|
+
element.insertBefore(newTextNode, oldNode || null);
|
|
243
340
|
}
|
|
244
341
|
}
|
|
245
342
|
} else {
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// 检查是否应该保留旧节点
|
|
250
|
-
if (!shouldPreserveElement(textNode)) {
|
|
251
|
-
element.removeChild(textNode);
|
|
252
|
-
}
|
|
253
|
-
// 如果应该保留,不删除(但可能仍需要添加新节点)
|
|
343
|
+
// 类型变化:文本 -> 元素
|
|
344
|
+
if (oldNode && !shouldPreserveElement(oldNode)) {
|
|
345
|
+
element.removeChild(oldNode);
|
|
254
346
|
}
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
347
|
+
if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
348
|
+
if (newChild.parentNode !== element) {
|
|
349
|
+
element.insertBefore(newChild, oldNode || null);
|
|
350
|
+
}
|
|
259
351
|
} else if (newChild instanceof DocumentFragment) {
|
|
260
|
-
element.
|
|
352
|
+
element.insertBefore(newChild, oldNode || null);
|
|
261
353
|
}
|
|
262
354
|
}
|
|
263
355
|
} else if (oldChild instanceof HTMLElement || oldChild instanceof SVGElement) {
|
|
356
|
+
// 关键修复:如果 oldNode 是应该保留的元素(手动创建的元素、第三方库注入的元素),跳过处理
|
|
357
|
+
// 这些元素不在 oldChildren 或 newChildren 中,应该在第二步被保留
|
|
358
|
+
if (oldNode && shouldPreserveElement(oldNode)) {
|
|
359
|
+
// 跳过应该保留的元素,继续处理下一个
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
264
363
|
// 如果是元素节点,检查是否是同一个元素
|
|
265
364
|
if (newChild === oldChild) {
|
|
266
|
-
//
|
|
365
|
+
// 同一个元素,不需要更新(元素内容会在 updateElement 中更新)
|
|
267
366
|
continue;
|
|
268
367
|
} else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
269
|
-
//
|
|
270
|
-
|
|
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
|
+
|
|
271
377
|
if (oldNode) {
|
|
272
|
-
//
|
|
378
|
+
// oldNode 存在(oldChild 在 DOM 中)
|
|
273
379
|
if (!shouldPreserveElement(oldNode)) {
|
|
274
|
-
//
|
|
380
|
+
// 可以替换
|
|
275
381
|
if (oldNode !== newChild) {
|
|
276
|
-
|
|
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
|
+
}
|
|
277
412
|
}
|
|
278
413
|
} else {
|
|
279
414
|
// 应该保留旧节点,只添加新节点(不替换)
|
|
280
415
|
if (newChild.parentNode !== element) {
|
|
281
|
-
|
|
416
|
+
// 如果 newChild 在其他父元素中,先移除
|
|
417
|
+
if (newChild.parentNode) {
|
|
418
|
+
newChild.parentNode.removeChild(newChild);
|
|
419
|
+
}
|
|
420
|
+
element.insertBefore(newChild, oldNode.nextSibling);
|
|
282
421
|
}
|
|
283
422
|
}
|
|
284
423
|
} else {
|
|
424
|
+
// oldNode 不存在(oldChild 不在 DOM 中),直接添加新元素
|
|
285
425
|
if (newChild.parentNode !== element) {
|
|
426
|
+
// 如果 newChild 在其他父元素中,先移除
|
|
427
|
+
if (newChild.parentNode) {
|
|
428
|
+
newChild.parentNode.removeChild(newChild);
|
|
429
|
+
}
|
|
286
430
|
element.appendChild(newChild);
|
|
287
431
|
}
|
|
288
432
|
}
|
|
289
433
|
} else {
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// 检查是否应该保留旧节点
|
|
294
|
-
if (!shouldPreserveElement(oldNode)) {
|
|
295
|
-
element.removeChild(oldNode);
|
|
296
|
-
}
|
|
297
|
-
// 如果应该保留,不删除(但可能仍需要添加新节点)
|
|
434
|
+
// 类型变化:元素 -> 文本
|
|
435
|
+
if (oldNode && !shouldPreserveElement(oldNode)) {
|
|
436
|
+
element.removeChild(oldNode);
|
|
298
437
|
}
|
|
299
438
|
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
300
|
-
|
|
439
|
+
const newTextNode = document.createTextNode(String(newChild));
|
|
440
|
+
element.insertBefore(newTextNode, oldNode?.nextSibling || null);
|
|
301
441
|
} else if (newChild instanceof DocumentFragment) {
|
|
302
|
-
element.
|
|
442
|
+
element.insertBefore(newChild, oldNode?.nextSibling || null);
|
|
303
443
|
}
|
|
304
444
|
}
|
|
305
445
|
}
|
|
@@ -316,11 +456,26 @@ export function updateChildren(
|
|
|
316
456
|
element.appendChild(document.createTextNode(String(newChild)));
|
|
317
457
|
} else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
318
458
|
// 确保子元素正确添加到当前父容器
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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);
|
|
323
476
|
}
|
|
477
|
+
// 添加 newChild 到当前 element 的末尾
|
|
478
|
+
element.appendChild(newChild);
|
|
324
479
|
} else if (newChild instanceof DocumentFragment) {
|
|
325
480
|
element.appendChild(newChild);
|
|
326
481
|
}
|
|
@@ -328,14 +483,113 @@ export function updateChildren(
|
|
|
328
483
|
|
|
329
484
|
// 移除多余子节点(阶段 5:正确处理元素保留)
|
|
330
485
|
// 关键:需要跳过"应该保留"的元素(第三方库注入的元素)
|
|
486
|
+
// 以及已经在 newChildren 中的元素(通过元素引用或 cache key 匹配)
|
|
331
487
|
const nodesToRemove: Node[] = [];
|
|
332
|
-
|
|
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++) {
|
|
333
565
|
const child = element.childNodes[i];
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
+
}
|
|
337
589
|
}
|
|
338
|
-
|
|
590
|
+
|
|
591
|
+
// 只有不应该保留且不在 newChildren 中的节点才添加到移除列表
|
|
592
|
+
nodesToRemove.push(child);
|
|
339
593
|
}
|
|
340
594
|
|
|
341
595
|
// 统一移除(从后往前移除,避免索引变化)
|
|
@@ -363,15 +617,17 @@ export function updateElement(
|
|
|
363
617
|
const oldProps = (oldMetadata?.props as Record<string, unknown>) || null;
|
|
364
618
|
const oldChildren = (oldMetadata?.children as JSXChildren[]) || [];
|
|
365
619
|
|
|
620
|
+
// 关键修复:在更新 DOM 之前先保存新的元数据
|
|
621
|
+
// 这样可以防止竞态条件:如果在更新过程中触发了另一个渲染,
|
|
622
|
+
// 新渲染会读取到正确的元数据,而不是过时的数据
|
|
623
|
+
cacheManager.setMetadata(element, {
|
|
624
|
+
props: newProps || {},
|
|
625
|
+
children: newChildren,
|
|
626
|
+
});
|
|
627
|
+
|
|
366
628
|
// 更新 props
|
|
367
629
|
updateProps(element, oldProps, newProps, tag);
|
|
368
630
|
|
|
369
631
|
// 更新 children
|
|
370
632
|
updateChildren(element, oldChildren, newChildren);
|
|
371
|
-
|
|
372
|
-
// 保存新的元数据
|
|
373
|
-
cacheManager.setMetadata(element, {
|
|
374
|
-
props: newProps || {},
|
|
375
|
-
children: newChildren,
|
|
376
|
-
});
|
|
377
633
|
}
|
package/src/web-component.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { h, type JSXChildren } from "./jsx-factory";
|
|
|
12
12
|
import { StyleManager } from "./styles/style-manager";
|
|
13
13
|
import { BaseComponent, type BaseComponentConfig } from "./base-component";
|
|
14
14
|
import { RenderContext } from "./render-context";
|
|
15
|
+
import { shouldPreserveElement } from "./utils/element-marking";
|
|
15
16
|
import { createLogger } from "@wsxjs/wsx-logger";
|
|
16
17
|
|
|
17
18
|
const logger = createLogger("WebComponent");
|
|
@@ -100,7 +101,9 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
100
101
|
// 渲染JSX内容到Shadow DOM
|
|
101
102
|
// render() 应该返回包含 slot 元素的内容(如果需要)
|
|
102
103
|
// 浏览器会自动将 Light DOM 中的内容分配到 slot
|
|
103
|
-
|
|
104
|
+
// 关键修复:使用 RenderContext.runInContext() 确保 h() 能够获取上下文
|
|
105
|
+
// 否则,首次渲染时创建的元素不会被标记 __wsxCacheKey,导致重复元素问题
|
|
106
|
+
const content = RenderContext.runInContext(this, () => this.render());
|
|
104
107
|
this.shadowRoot.appendChild(content);
|
|
105
108
|
}
|
|
106
109
|
|
|
@@ -212,10 +215,24 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
212
215
|
// 添加新内容
|
|
213
216
|
this.shadowRoot.appendChild(content);
|
|
214
217
|
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
// 移除旧内容(保留样式元素和未标记元素)
|
|
219
|
+
// 关键修复:使用 shouldPreserveElement() 来保护第三方库注入的元素
|
|
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
|
+
});
|
|
219
236
|
oldChildren.forEach((child) => child.remove());
|
|
220
237
|
|
|
221
238
|
// 恢复焦点状态
|