@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.
@@ -56,7 +56,44 @@ export function h(
56
56
  }
57
57
 
58
58
  // 无上下文:使用旧逻辑(向后兼容)
59
- return createElementWithPropsAndChildren(tag, props, children);
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
- return createElementWithPropsAndChildren(tag, props, children);
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
  /**
@@ -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 { flattenChildren, type JSXChildren } from "./dom-utils";
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
- if (JSON.stringify(oldValue) === JSON.stringify(newValue)) {
201
- continue;
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 = flattenChildren(oldChildren);
220
- const flatNew = flattenChildren(newChildren);
257
+ const flatOld = flattenChildrenSafe(oldChildren);
258
+ const flatNew = flattenChildrenSafe(newChildren);
221
259
 
222
- // 阶段 4 简化版:只处理相同数量的子节点
223
- const minLength = Math.min(flatOld.length, flatNew.length);
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 textNode = element.childNodes[i];
234
- if (textNode && textNode.nodeType === Node.TEXT_NODE) {
235
- textNode.textContent = String(newChild);
236
- } else {
237
- // 替换为新的文本节点
238
- const newTextNode = document.createTextNode(String(newChild));
239
- if (textNode) {
240
- element.replaceChild(newTextNode, textNode);
241
- } else {
242
- element.appendChild(newTextNode);
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
- const textNode = element.childNodes[i];
248
- if (textNode) {
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.appendChild(newChild);
329
+ element.insertBefore(newChild, oldNode || null);
261
330
  }
262
331
  }
263
- } else if (oldChild instanceof HTMLElement || oldChild instanceof SVGElement) {
264
- // 如果是元素节点,检查是否是同一个元素
265
- if (newChild === oldChild) {
266
- // 同一个元素,不需要更新
267
- continue;
268
- } else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
269
- // 不同的元素,替换
270
- const oldNode = element.childNodes[i];
271
- if (oldNode) {
272
- // 检查是否应该保留旧节点
273
- if (!shouldPreserveElement(oldNode)) {
274
- // 只有当新子元素不在当前位置时才替换
275
- if (oldNode !== newChild) {
276
- element.replaceChild(newChild, oldNode);
277
- }
278
- } else {
279
- // 应该保留旧节点,只添加新节点(不替换)
280
- if (newChild.parentNode !== element) {
281
- element.appendChild(newChild);
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
- if (newChild.parentNode !== element) {
286
- element.appendChild(newChild);
360
+ // 如果没有 cacheManager,假设 h() 已经更新了子元素
361
+ if (oldNode === newChild && newChild.parentNode === element) {
362
+ continue;
287
363
  }
288
364
  }
289
- } else {
290
- // 类型变化:替换节点
291
- const oldNode = element.childNodes[i];
292
- if (oldNode) {
293
- // 检查是否应该保留旧节点
294
- if (!shouldPreserveElement(oldNode)) {
295
- element.removeChild(oldNode);
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
- element.appendChild(document.createTextNode(String(newChild)));
378
+ const newTextNode = document.createTextNode(String(newChild));
379
+ element.insertBefore(newTextNode, oldNode?.nextSibling || null);
301
380
  } else if (newChild instanceof DocumentFragment) {
302
- element.appendChild(newChild);
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
- const newChild = flatNew[i];
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
- // 移除多余子节点(阶段 5:正确处理元素保留)
330
- // 关键:需要跳过"应该保留"的元素(第三方库注入的元素)
331
- const nodesToRemove: Node[] = [];
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
- for (let i = nodesToRemove.length - 1; i >= 0; i--) {
343
- const node = nodesToRemove[i];
344
- if (node.parentNode === element) {
345
- element.removeChild(node);
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
- // 更新 props
367
- updateProps(element, oldProps, newProps, tag);
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
  }