@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.
- package/dist/chunk-2ER76KOQ.mjs +1422 -0
- package/dist/chunk-FAPFH5ON.mjs +1372 -0
- package/dist/chunk-NEHWERG6.mjs +1424 -0
- package/dist/chunk-PIKDVFOA.mjs +1337 -0
- package/dist/chunk-PNIWQQN6.mjs +1330 -0
- package/dist/chunk-PP54HBAY.mjs +1337 -0
- package/dist/chunk-U74WFVRE.mjs +1308 -0
- package/dist/chunk-UTWWJJ4C.mjs +1360 -0
- package/dist/index.js +1767 -1906
- package/dist/index.mjs +178 -336
- package/dist/jsx-runtime.js +168 -148
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +168 -148
- package/dist/jsx.mjs +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/base-component.ts +8 -305
- package/src/index.ts +3 -0
- package/src/jsx-factory.ts +3 -1
- package/src/light-component.ts +117 -99
- package/src/utils/cache-key.ts +17 -2
- package/src/utils/dom-utils.ts +14 -42
- package/src/utils/element-marking.ts +5 -3
- package/src/utils/element-update.ts +108 -154
- package/src/utils/update-children-helpers.ts +99 -83
- package/src/web-component.ts +108 -96
package/src/light-component.ts
CHANGED
|
@@ -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.
|
|
98
|
-
(
|
|
98
|
+
const childrenToRemove = Array.from(this.childNodes).filter(
|
|
99
|
+
(node) => node !== styleElement
|
|
99
100
|
);
|
|
100
|
-
childrenToRemove.forEach((
|
|
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.
|
|
174
|
-
|
|
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
|
|
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.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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():
|
|
314
|
+
private getJSXChildren(): Node[] {
|
|
301
315
|
// 在 connectedCallback 中标记的 JSX children
|
|
302
|
-
// 使用 data
|
|
303
|
-
const jsxChildren = Array.from(this.
|
|
304
|
-
|
|
305
|
-
(child)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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.
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
}
|
package/src/utils/cache-key.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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;
|
package/src/utils/dom-utils.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
):
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|