@wsxjs/wsx-core 0.0.28 → 0.1.0

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.
@@ -10,8 +10,9 @@
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";
14
13
  import { createLogger } from "@wsxjs/wsx-logger";
14
+ import { updateProps, updateChildren } from "./utils/element-update";
15
+ import { shouldPreserveElement } from "./utils/element-marking";
15
16
 
16
17
  const logger = createLogger("LightComponent");
17
18
 
@@ -94,10 +95,10 @@ export abstract class LightComponent extends BaseComponent {
94
95
  } else {
95
96
  // 没有内容,需要渲染
96
97
  // 清空旧内容(保留样式元素)
97
- const childrenToRemove = Array.from(this.children).filter(
98
- (child) => child !== styleElement
98
+ const childrenToRemove = Array.from(this.childNodes).filter(
99
+ (node) => node !== styleElement
99
100
  );
100
- childrenToRemove.forEach((child) => child.remove());
101
+ childrenToRemove.forEach((node) => node.remove());
101
102
 
102
103
  // 渲染JSX内容到Light DOM
103
104
  const content = RenderContext.runInContext(this, () => this.render());
@@ -156,6 +157,10 @@ export abstract class LightComponent extends BaseComponent {
156
157
  return HTMLElement.prototype.querySelectorAll.call(this, selector) as NodeListOf<T>;
157
158
  }
158
159
 
160
+ /**
161
+ * 递归协调子元素
162
+ * 更新现有子元素的属性和内容,而不是替换整个子树
163
+ */
159
164
  /**
160
165
  * 内部重渲染实现
161
166
  * 包含从 rerender() 方法迁移的实际渲染逻辑
@@ -170,9 +175,8 @@ export abstract class LightComponent extends BaseComponent {
170
175
  return;
171
176
  }
172
177
 
173
- // 1. 捕获焦点状态(在 DOM 替换之前)
174
- const focusState = this.captureFocusState();
175
- this._pendingFocusState = focusState;
178
+ // 1. (已移除) 捕获焦点状态
179
+ // 根据 RFC 0061,手动焦点管理已被弃用,核心协调引擎现在负责通过 DOM 复用来保持焦点。
176
180
 
177
181
  // 2. 保存 JSX children(通过 JSX factory 直接添加的 children)
178
182
  // 这些 children 不是 render() 返回的内容,应该保留
@@ -180,23 +184,7 @@ export abstract class LightComponent extends BaseComponent {
180
184
 
181
185
  try {
182
186
  // 3. 重新渲染JSX内容
183
- const content = RenderContext.runInContext(this, () => this.render());
184
-
185
- // 4. 在添加到 DOM 之前恢复值,避免浏览器渲染状态值
186
- if (focusState && focusState.key && focusState.value !== undefined) {
187
- const target = content.querySelector(
188
- `[data-wsx-key="${focusState.key}"]`
189
- ) as HTMLElement;
190
-
191
- if (target) {
192
- if (
193
- target instanceof HTMLInputElement ||
194
- target instanceof HTMLTextAreaElement
195
- ) {
196
- target.value = focusState.value;
197
- }
198
- }
199
- }
187
+ const newContent = RenderContext.runInContext(this, () => this.render());
200
188
 
201
189
  // 5. 确保样式元素存在
202
190
  const stylesToApply = this._autoStyles || this.config.styles;
@@ -218,71 +206,97 @@ export abstract class LightComponent extends BaseComponent {
218
206
  }
219
207
  }
220
208
 
221
- // 6. 使用 requestAnimationFrame 批量执行 DOM 操作
222
- requestAnimationFrame(() => {
223
- // 先添加新内容
224
- this.appendChild(content);
209
+ // 6. 执行 DOM 操作
210
+ // 获取当前的 childNodes(包括文本节点,排除样式元素和 JSX children)
211
+ const allShadowChildren = Array.from(this.childNodes);
212
+ const oldChildren = allShadowChildren.filter((child) => {
213
+ // 排除样式元素
214
+ if (
215
+ stylesToApply &&
216
+ child instanceof HTMLStyleElement &&
217
+ child.getAttribute("data-wsx-light-component") === styleName
218
+ ) {
219
+ return false;
220
+ }
221
+ // 排除 JSX children (RFC: 这里的 jsxChildren 现在包含 Text 节点)
222
+ if (jsxChildren.includes(child as any)) {
223
+ return false;
224
+ }
225
+ // 排除保留元素 (RFC 0058)
226
+ if (shouldPreserveElement(child)) {
227
+ return false;
228
+ }
229
+ return true;
230
+ });
225
231
 
226
- // 移除旧内容(保留 JSX children、样式元素和未标记元素)
227
- // 关键修复:使用 shouldPreserveElement() 来保护手动创建的元素(如第三方库注入的元素)
228
- const oldChildren = Array.from(this.children).filter((child) => {
229
- // 保留新添加的内容
230
- if (child === content) {
231
- return false;
232
- }
233
- // 保留样式元素
234
- if (
235
- stylesToApply &&
236
- child instanceof HTMLStyleElement &&
237
- child.getAttribute("data-wsx-light-component") === styleName
238
- ) {
239
- return false;
240
- }
241
- // 保留 JSX children(通过 JSX factory 直接添加的 children)
242
- if (child instanceof HTMLElement && jsxChildren.includes(child)) {
243
- return false;
244
- }
245
- // 保留未标记的元素(手动创建的元素、第三方库注入的元素)
246
- // 这是 RFC 0037 Phase 5 的核心:保护未标记元素
247
- if (shouldPreserveElement(child)) {
248
- return false;
232
+ // 7. True DOM Reconciliation (RFC 0058) for Light DOM
233
+ // Similar to WebComponent but handling list of children directly
234
+
235
+ // Case 1: Single Root => Single Root
236
+ if (
237
+ oldChildren.length === 1 &&
238
+ newContent instanceof HTMLElement &&
239
+ oldChildren[0] instanceof HTMLElement &&
240
+ oldChildren[0].tagName === newContent.tagName
241
+ ) {
242
+ const oldRoot = oldChildren[0] as HTMLElement;
243
+ const newRoot = newContent;
244
+
245
+ if (oldRoot !== newRoot) {
246
+ const cacheManager = RenderContext.getDOMCache();
247
+ if (cacheManager) {
248
+ const oldMetadata = cacheManager.getMetadata(oldRoot);
249
+ const newMetadata = cacheManager.getMetadata(newRoot);
250
+ if (oldMetadata && newMetadata) {
251
+ updateProps(
252
+ oldRoot,
253
+ oldMetadata.props as Record<string, unknown>,
254
+ newMetadata.props as Record<string, unknown>,
255
+ oldRoot.tagName
256
+ );
257
+ updateChildren(
258
+ oldRoot,
259
+ oldMetadata.children as JSXChildren[],
260
+ newMetadata.children as JSXChildren[],
261
+ cacheManager
262
+ );
263
+ } else {
264
+ oldRoot.replaceWith(newRoot);
265
+ }
266
+ } else {
267
+ oldRoot.replaceWith(newRoot);
249
268
  }
250
- return true;
251
- });
269
+ }
270
+ } else {
271
+ // Case 2: Multi-root or mismatch
272
+ // For now, doing smart replacement
252
273
  oldChildren.forEach((child) => child.remove());
274
+ this.appendChild(newContent);
275
+ }
253
276
 
254
- // 确保样式元素存在并在第一个位置
255
- // 关键修复:在元素复用场景中,如果 _autoStyles 存在但样式元素被意外移除,需要重新创建
256
- if (stylesToApply) {
257
- let styleElement = this.querySelector(
258
- `style[data-wsx-light-component="${styleName}"]`
259
- ) as HTMLStyleElement | null;
260
-
261
- if (!styleElement) {
262
- // 样式元素被意外移除,重新创建
263
- styleElement = document.createElement("style");
264
- styleElement.setAttribute("data-wsx-light-component", styleName);
265
- styleElement.textContent = stylesToApply;
266
- this.insertBefore(styleElement, this.firstChild);
267
- } else if (styleElement.textContent !== stylesToApply) {
268
- // 样式内容已变化,更新
269
- styleElement.textContent = stylesToApply;
270
- } else if (styleElement !== this.firstChild) {
271
- // 样式元素存在但不在第一个位置,移动到第一个位置
272
- this.insertBefore(styleElement, this.firstChild);
273
- }
277
+ // 确保样式元素存在并在第一个位置 (re-verify)
278
+ if (stylesToApply) {
279
+ let styleEl = this.querySelector(
280
+ `style[data-wsx-light-component="${styleName}"]`
281
+ ) as HTMLStyleElement | null;
282
+
283
+ if (!styleEl) {
284
+ styleEl = document.createElement("style");
285
+ styleEl.setAttribute("data-wsx-light-component", styleName);
286
+ styleEl.textContent = stylesToApply;
287
+ this.insertBefore(styleEl, this.firstChild);
288
+ } else if (styleEl !== this.firstChild) {
289
+ this.insertBefore(styleEl, this.firstChild);
274
290
  }
291
+ }
275
292
 
276
- // 恢复焦点状态
277
- requestAnimationFrame(() => {
278
- this.restoreFocusState(focusState);
279
- this._pendingFocusState = null;
280
- // 调用 onRendered 生命周期钩子
281
- this.onRendered?.();
282
- // onRendered() 完成后清除渲染标志,允许后续的 scheduleRerender()
283
- this._isRendering = false;
284
- });
285
- });
293
+ // 恢复焦点状态 (已根据 RFC 0061 移除)
294
+ // this.restoreFocusState(focusState);
295
+ // this._pendingFocusState = null;
296
+ // 调用 onRendered 生命周期钩子
297
+ this.onRendered?.();
298
+ // 在 onRendered() 完成后清除渲染标志,允许后续的 scheduleRerender()
299
+ this._isRendering = false;
286
300
  } catch (error) {
287
301
  logger.error(`[${this.constructor.name}] Error in _rerender:`, error);
288
302
  this.renderError(error);
@@ -297,16 +311,18 @@ export abstract class LightComponent extends BaseComponent {
297
311
  * 在 Light DOM 中,JSX children 是通过 JSX factory 直接添加到组件元素的
298
312
  * 这些 children 不是 render() 返回的内容,应该保留
299
313
  */
300
- private getJSXChildren(): HTMLElement[] {
314
+ private getJSXChildren(): Node[] {
301
315
  // 在 connectedCallback 中标记的 JSX children
302
- // 使用 data 属性标记:data-wsx-jsx-child="true"
303
- const jsxChildren = Array.from(this.children)
304
- .filter(
305
- (child) =>
306
- child instanceof HTMLElement &&
307
- child.getAttribute("data-wsx-jsx-child") === "true"
308
- )
309
- .map((child) => child as HTMLElement);
316
+ // 使用 data 属性或内部属性标记
317
+ const jsxChildren = Array.from(this.childNodes).filter((node) => {
318
+ if (node instanceof HTMLElement) {
319
+ return node.getAttribute("data-wsx-jsx-child") === "true";
320
+ }
321
+ if (node.nodeType === Node.TEXT_NODE) {
322
+ return (node as Text & { __wsxJsxChild?: boolean }).__wsxJsxChild === true;
323
+ }
324
+ return false;
325
+ });
310
326
 
311
327
  return jsxChildren;
312
328
  }
@@ -323,13 +339,15 @@ export abstract class LightComponent extends BaseComponent {
323
339
  `style[data-wsx-light-component="${styleName}"]`
324
340
  ) as HTMLStyleElement | null;
325
341
 
326
- Array.from(this.children).forEach((child) => {
327
- if (
328
- child instanceof HTMLElement &&
329
- child !== styleElement &&
330
- !(child instanceof HTMLSlotElement)
331
- ) {
332
- child.setAttribute("data-wsx-jsx-child", "true");
342
+ Array.from(this.childNodes).forEach((node) => {
343
+ if (node !== styleElement && !(node instanceof HTMLSlotElement)) {
344
+ if (node instanceof HTMLElement) {
345
+ node.setAttribute("data-wsx-jsx-child", "true");
346
+ } else if (node.nodeType === Node.TEXT_NODE) {
347
+ (node as Text & { __wsxManaged?: boolean }).__wsxManaged = true;
348
+ // For text nodes, we also use a custom property to identify them as JSX children
349
+ (node as Text & { __wsxJsxChild?: boolean }).__wsxJsxChild = true;
350
+ }
333
351
  }
334
352
  });
335
353
  }
@@ -22,10 +22,16 @@ const componentElementCounters = new WeakMap<BaseComponent, number>();
22
22
 
23
23
  /**
24
24
  * Component ID cache (using WeakMap to avoid memory leaks)
25
- * Caches component IDs to avoid recomputing them on every render.
25
+ * Caches component IDs to avoid recomputing them on every call.
26
26
  */
27
27
  const componentIdCache = new WeakMap<BaseComponent, string>();
28
28
 
29
+ /**
30
+ * Auto-incremental instance counter for components without a manual ID.
31
+ */
32
+ const instanceAutoIds = new WeakMap<BaseComponent, number>();
33
+ let globalAutoId = 0;
34
+
29
35
  /**
30
36
  * Generates a cache key for a DOM element.
31
37
  *
@@ -111,7 +117,16 @@ export function getComponentId(): string {
111
117
 
112
118
  // Compute and cache
113
119
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
- const instanceId = (component as any)._instanceId || "default";
120
+ let instanceId = (component as any)._wsxInstanceId;
121
+
122
+ // 如果没有显示 ID,分配一个唯一的自动 ID (RFC 0037 增强)
123
+ if (instanceId === undefined || instanceId === null) {
124
+ if (!instanceAutoIds.has(component)) {
125
+ instanceAutoIds.set(component, ++globalAutoId);
126
+ }
127
+ instanceId = String(instanceAutoIds.get(component));
128
+ }
129
+
115
130
  cachedId = `${component.constructor.name}:${instanceId}`;
116
131
  componentIdCache.set(component, cachedId);
117
132
  return cachedId;
@@ -11,6 +11,7 @@ export type JSXChildren =
11
11
  | HTMLElement
12
12
  | SVGElement
13
13
  | DocumentFragment
14
+ | Node
14
15
  | JSXChildren[]
15
16
  | null
16
17
  | undefined
@@ -51,9 +52,15 @@ export function parseHTMLToNodes(html: string): (HTMLElement | SVGElement | stri
51
52
  // Text nodes are converted to strings, elements are kept as-is
52
53
  return Array.from(temp.childNodes).map((node) => {
53
54
  if (node instanceof HTMLElement || node instanceof SVGElement) {
55
+ // 关键修复:标记解析出的元素为框架管理
56
+ // 这确保了 shouldPreserveElement 不会错误地保留这些元素
57
+ // 从而防止了在频繁重渲染(如 Markdown 输入)时的元素堆积
58
+ (node as HTMLElement & { __wsxManaged?: boolean }).__wsxManaged = true;
54
59
  return node;
55
60
  } else {
56
61
  // Convert text nodes and other node types to strings
62
+ // Note: When these strings are processed by appendChildrenToElement,
63
+ // they will be wraped in managed text nodes.
57
64
  return node.textContent || "";
58
65
  }
59
66
  });
@@ -79,7 +86,8 @@ export function isHTMLString(str: string): boolean {
79
86
  const looksLikeMath = /^[^<]*<[^>]*>[^>]*$/.test(trimmed) && !htmlTagPattern.test(trimmed);
80
87
  if (looksLikeMath) return false;
81
88
 
82
- return htmlTagPattern.test(trimmed);
89
+ const result = htmlTagPattern.test(trimmed);
90
+ return result;
83
91
  }
84
92
 
85
93
  /**
@@ -94,7 +102,7 @@ export function flattenChildren(
94
102
  children: JSXChildren[],
95
103
  skipHTMLDetection: boolean = false,
96
104
  depth: number = 0
97
- ): (string | number | HTMLElement | SVGElement | DocumentFragment | boolean | null | undefined)[] {
105
+ ): JSXChildren[] {
98
106
  // 防止无限递归:如果深度超过 10,停止处理
99
107
  if (depth > 10) {
100
108
  console.warn(
@@ -105,16 +113,7 @@ export function flattenChildren(
105
113
  typeof child === "string" || typeof child === "number"
106
114
  );
107
115
  }
108
- const result: (
109
- | string
110
- | number
111
- | HTMLElement
112
- | SVGElement
113
- | DocumentFragment
114
- | boolean
115
- | null
116
- | undefined
117
- )[] = [];
116
+ const result: JSXChildren[] = [];
118
117
 
119
118
  for (const child of children) {
120
119
  if (child === null || child === undefined || child === false) {
@@ -128,30 +127,17 @@ export function flattenChildren(
128
127
  result.push(child);
129
128
  } else if (isHTMLString(child)) {
130
129
  // 自动检测HTML字符串并转换为DOM节点
131
- // 使用 try-catch 防止解析失败导致崩溃
132
130
  try {
133
131
  const nodes = parseHTMLToNodes(child);
134
- // 递归处理转换后的节点数组,标记为已解析,避免再次检测HTML
135
- // parseHTMLToNodes 返回的字符串是纯文本节点,不应该再次被检测为HTML
136
- // 但是为了安全,我们仍然设置 skipHTMLDetection = true
137
132
  if (nodes.length > 0) {
138
133
  // 直接添加解析后的节点,不再递归处理(避免无限递归)
139
- // parseHTMLToNodes 已经完成了所有解析工作
140
134
  for (const node of nodes) {
141
- if (typeof node === "string") {
142
- // 文本节点直接添加,不再检测 HTML(已解析)
143
- result.push(node);
144
- } else {
145
- // DOM 元素直接添加
146
- result.push(node);
147
- }
135
+ result.push(node);
148
136
  }
149
137
  } else {
150
- // 如果解析失败,回退到纯文本
151
138
  result.push(child);
152
139
  }
153
140
  } catch (error) {
154
- // 如果解析失败,回退到纯文本,避免崩溃
155
141
  console.warn("[WSX] Failed to parse HTML string, treating as text:", error);
156
142
  result.push(child);
157
143
  }
@@ -160,24 +146,10 @@ export function flattenChildren(
160
146
  }
161
147
  } else if (child instanceof DocumentFragment) {
162
148
  // 递归处理 DocumentFragment 中的子节点
163
- // 注意:Array.from 会创建子节点的引用副本,
164
- // 这样即使 Fragment 在后续过程中被 appendChild 清空,
165
- // 我们的 flat children 列表仍然持有正确的节点引用。
166
- // 关键:不能递归调用 flattenChildren(Array.from(child.childNodes)),
167
- // 因为 DocumentFragment 本身不支持 skipHTMLDetection。
168
- // 我们直接将其子节点展平到当前结果中。
169
149
  const fragmentChildren = Array.from(child.childNodes);
170
- for (const fragChild of fragmentChildren) {
171
- if (fragChild instanceof HTMLElement || fragChild instanceof SVGElement) {
172
- result.push(fragChild);
173
- } else if (fragChild.nodeType === Node.TEXT_NODE) {
174
- result.push(fragChild.textContent || "");
175
- } else if (fragChild instanceof DocumentFragment) {
176
- // 处理嵌套 Fragment(防御性编程)
177
- result.push(...flattenChildren([fragChild], skipHTMLDetection, depth + 1));
178
- }
179
- }
150
+ result.push(...flattenChildren(fragmentChildren, skipHTMLDetection, depth + 1));
180
151
  } else {
152
+ // 保持 Node 引用
181
153
  result.push(child);
182
154
  }
183
155
  }
@@ -68,15 +68,17 @@ export function shouldPreserveElement(element: Node): boolean {
68
68
  }
69
69
 
70
70
  // 规则 2: 没有标记的元素保留(自定义元素、第三方库注入)
71
- if (!isCreatedByH(element)) {
71
+ // 关键修正:除了检查是否由 h() 创建,还要检查是否被框架显式标记为托管 (__wsxManaged)
72
+ // 这主要用于处理从 HTML 字符串解析出的元素 (parseHTMLToNodes)
73
+ if (!isCreatedByH(element) && (element as any).__wsxManaged !== true) {
72
74
  return true;
73
75
  }
74
76
 
75
77
  // 规则 3: 显式标记保留
76
- if (element.hasAttribute("data-wsx-preserve")) {
78
+ if (element instanceof HTMLElement && element.hasAttribute("data-wsx-preserve")) {
77
79
  return true;
78
80
  }
79
81
 
80
- // 规则 4: 由 h() 创建的元素 → 不保留(由框架管理)
82
+ // 规则 4: 由 h() 创建或被标记为托管的元素 → 不保留(由框架管理)
81
83
  return false;
82
84
  }