@wsxjs/wsx-core 0.0.27 → 0.0.30
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-34PNC5CJ.mjs +1307 -0
- package/dist/chunk-FAPFH5ON.mjs +1372 -0
- package/dist/chunk-HWJ7GZD6.mjs +1327 -0
- package/dist/chunk-U74WFVRE.mjs +1308 -0
- package/dist/chunk-UTWWJJ4C.mjs +1360 -0
- package/dist/chunk-ZY36MEHX.mjs +1306 -0
- package/dist/index.js +259 -138
- package/dist/index.mjs +49 -32
- package/dist/jsx-runtime.js +163 -107
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +163 -107
- package/dist/jsx.mjs +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/base-component.ts +14 -10
- package/src/index.ts +2 -0
- package/src/jsx-factory.ts +3 -1
- package/src/light-component.ts +61 -31
- package/src/utils/dom-utils.ts +32 -1
- package/src/utils/element-creation.ts +3 -1
- package/src/utils/element-marking.ts +9 -5
- package/src/utils/element-update.ts +178 -127
- package/src/utils/update-children-helpers.ts +133 -66
- package/src/web-component.ts +6 -5
|
@@ -15,6 +15,14 @@ export function collectPreservedElements(element: HTMLElement | SVGElement): Nod
|
|
|
15
15
|
const preserved: Node[] = [];
|
|
16
16
|
for (let i = 0; i < element.childNodes.length; i++) {
|
|
17
17
|
const child = element.childNodes[i];
|
|
18
|
+
|
|
19
|
+
// RFC 0048 & RFC 0053 关键修复:文本节点通常不应该被 collectPreservedElements 捕获
|
|
20
|
+
// 因为它们会通过 updateChildren 的主循环进行 reconcile
|
|
21
|
+
// 只有当父元素本身是保留元素时,文本节点才应该被保留(但在 updateChildren 中,这种情况会跳过处理)
|
|
22
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
if (shouldPreserveElement(child)) {
|
|
19
27
|
preserved.push(child);
|
|
20
28
|
}
|
|
@@ -77,20 +85,20 @@ export function findElementNode(
|
|
|
77
85
|
*/
|
|
78
86
|
export function findTextNode(
|
|
79
87
|
parent: HTMLElement | SVGElement,
|
|
80
|
-
domIndex: { value: number }
|
|
88
|
+
domIndex: { value: number },
|
|
89
|
+
processedNodes: Set<Node>
|
|
81
90
|
): Node | null {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
// RFC 0053: 从当前索引开始查找第一个未被处理的、框架管理的文本节点
|
|
92
|
+
for (let i = domIndex.value; i < parent.childNodes.length; i++) {
|
|
93
|
+
const node = parent.childNodes[i];
|
|
94
|
+
if (
|
|
95
|
+
node.nodeType === Node.TEXT_NODE &&
|
|
96
|
+
(node as any).__wsxManaged === true &&
|
|
97
|
+
!processedNodes.has(node)
|
|
98
|
+
) {
|
|
99
|
+
domIndex.value = i + 1;
|
|
100
|
+
return node;
|
|
90
101
|
}
|
|
91
|
-
// 跳过元素节点和其他类型的节点(它们会在自己的迭代中处理)
|
|
92
|
-
// 关键:必须递增 domIndex,否则会无限循环
|
|
93
|
-
domIndex.value++;
|
|
94
102
|
}
|
|
95
103
|
return null;
|
|
96
104
|
}
|
|
@@ -117,45 +125,52 @@ export function shouldUpdateTextNode(
|
|
|
117
125
|
export function updateOrCreateTextNode(
|
|
118
126
|
parent: HTMLElement | SVGElement,
|
|
119
127
|
oldNode: Node | null,
|
|
120
|
-
newText: string
|
|
128
|
+
newText: string,
|
|
129
|
+
insertBeforeNode?: Node | null
|
|
121
130
|
): Node {
|
|
131
|
+
// RFC 0048 & RFC 0053 关键修复:如果 parent 是保留元素(第三方组件),跳过处理
|
|
132
|
+
// 防止框架错误地管理第三方组件内部的文本节点
|
|
133
|
+
if (shouldPreserveElement(parent)) {
|
|
134
|
+
// 如果 parent 是保留元素,直接返回 oldNode(如果存在)或创建新节点但不插入
|
|
135
|
+
// 注意:这种情况下,文本节点应该由第三方组件自己管理
|
|
136
|
+
if (oldNode && oldNode.nodeType === Node.TEXT_NODE) {
|
|
137
|
+
return oldNode;
|
|
138
|
+
}
|
|
139
|
+
// 对于保留元素,我们不应该插入文本节点,但为了兼容性,返回一个虚拟节点
|
|
140
|
+
// 实际上,这种情况不应该发生,因为保留元素的子节点不应该被框架处理
|
|
141
|
+
return document.createTextNode(newText);
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
if (oldNode && oldNode.nodeType === Node.TEXT_NODE) {
|
|
123
145
|
// 只有当文本内容不同时才更新
|
|
124
146
|
if (oldNode.textContent !== newText) {
|
|
125
147
|
oldNode.textContent = newText;
|
|
126
148
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!oldNode) {
|
|
133
|
-
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
134
|
-
const node = parent.childNodes[i];
|
|
135
|
-
// 关键修复:只检查直接子文本节点,确保 node.parentNode === parent
|
|
136
|
-
// 这样可以防止将元素内部的文本节点(如 span 内部的文本节点)误判为独立的文本节点
|
|
137
|
-
if (
|
|
138
|
-
node.nodeType === Node.TEXT_NODE &&
|
|
139
|
-
node.parentNode === parent &&
|
|
140
|
-
node.textContent === newText
|
|
141
|
-
) {
|
|
142
|
-
// 找到相同内容的直接子文本节点,返回它而不是创建新节点
|
|
143
|
-
return node;
|
|
144
|
-
}
|
|
149
|
+
// RFC 0053 扩展:确保节点在正确的位置
|
|
150
|
+
// 如果指定了插入位置且当前节点不在该位置,则移动它
|
|
151
|
+
if (insertBeforeNode !== undefined) {
|
|
152
|
+
if (oldNode !== insertBeforeNode && oldNode.nextSibling !== insertBeforeNode) {
|
|
153
|
+
parent.insertBefore(oldNode, insertBeforeNode);
|
|
145
154
|
}
|
|
146
155
|
}
|
|
156
|
+
return oldNode;
|
|
157
|
+
} else {
|
|
158
|
+
// RFC 0048 & RFC 0053 关键修复:文本节点应该基于位置匹配,而不是内容匹配
|
|
159
|
+
// 当 oldNode 为 null 时,直接创建新节点,不进行内容匹配搜索
|
|
160
|
+
// 这防止了日历等场景中相同内容在不同位置被错误匹配的问题
|
|
161
|
+
// 例如:3 月的 "30" 在底部行,4 月的第一个日期是 "1",不应该将 "30" 错误匹配到 "1" 的位置
|
|
162
|
+
// 如果 oldNode 为 null,说明在当前位置没有找到对应的文本节点,应该创建新节点
|
|
147
163
|
|
|
148
|
-
// 如果没有找到相同内容的文本节点,创建新节点
|
|
149
164
|
const newTextNode = document.createTextNode(newText);
|
|
165
|
+
(newTextNode as any).__wsxManaged = true; // 标记为框架管理
|
|
150
166
|
if (oldNode && !shouldPreserveElement(oldNode)) {
|
|
151
167
|
parent.replaceChild(newTextNode, oldNode);
|
|
152
168
|
} else {
|
|
153
|
-
parent.insertBefore(newTextNode,
|
|
169
|
+
parent.insertBefore(newTextNode, insertBeforeNode ?? null);
|
|
154
170
|
}
|
|
155
171
|
return newTextNode;
|
|
156
172
|
}
|
|
157
173
|
}
|
|
158
|
-
|
|
159
174
|
/**
|
|
160
175
|
* 移除节点(如果不应该保留)
|
|
161
176
|
*/
|
|
@@ -192,7 +207,8 @@ export function replaceOrInsertElementAtPosition(
|
|
|
192
207
|
parent: HTMLElement | SVGElement,
|
|
193
208
|
newChild: HTMLElement | SVGElement,
|
|
194
209
|
oldNode: Node | null,
|
|
195
|
-
targetNextSibling: Node | null
|
|
210
|
+
targetNextSibling: Node | null,
|
|
211
|
+
processedNodes?: Set<Node>
|
|
196
212
|
): void {
|
|
197
213
|
// 如果新元素已经在其他父元素中,先移除
|
|
198
214
|
if (newChild.parentNode && newChild.parentNode !== parent) {
|
|
@@ -200,11 +216,15 @@ export function replaceOrInsertElementAtPosition(
|
|
|
200
216
|
}
|
|
201
217
|
|
|
202
218
|
// 检查元素是否已经在正确位置
|
|
219
|
+
// 1. 如果 newChild.nextSibling === targetNextSibling,说明在正确位置的前面
|
|
220
|
+
// 2. 关键修复:如果 targetNextSibling === newChild,说明 newChild 就在目标位置(它本身就是下一个节点)
|
|
203
221
|
const isInCorrectPosition =
|
|
204
|
-
newChild.parentNode === parent &&
|
|
222
|
+
newChild.parentNode === parent &&
|
|
223
|
+
(newChild.nextSibling === targetNextSibling || targetNextSibling === newChild);
|
|
205
224
|
|
|
206
225
|
if (isInCorrectPosition) {
|
|
207
226
|
// 已经在正确位置,不需要移动
|
|
227
|
+
if (processedNodes) processedNodes.add(newChild);
|
|
208
228
|
return;
|
|
209
229
|
}
|
|
210
230
|
|
|
@@ -212,25 +232,40 @@ export function replaceOrInsertElementAtPosition(
|
|
|
212
232
|
if (newChild.parentNode === parent) {
|
|
213
233
|
// insertBefore 会自动处理:如果元素已经在 DOM 中,会先移除再插入到新位置
|
|
214
234
|
parent.insertBefore(newChild, targetNextSibling);
|
|
235
|
+
if (processedNodes) processedNodes.add(newChild);
|
|
215
236
|
return;
|
|
216
237
|
}
|
|
217
238
|
|
|
218
|
-
// 元素不在 parent
|
|
239
|
+
// 元素不在 parent 中,或者需要替换
|
|
219
240
|
if (oldNode && oldNode.parentNode === parent && !shouldPreserveElement(oldNode)) {
|
|
220
241
|
if (oldNode !== newChild) {
|
|
242
|
+
// RFC 0048 & RFC 0053 关键修正:特殊处理从 HTML 字符串解析出的元素
|
|
243
|
+
// 如果旧节点和新节点都没有 cache key (说明是 HTML 解析出的) 且内容相同
|
|
244
|
+
// 则保留旧节点,不进行替换。这解决了 Markdown 渲染中的 DOM 抖动。
|
|
245
|
+
const oldCacheKey = getElementCacheKey(oldNode as HTMLElement | SVGElement);
|
|
246
|
+
const newCacheKey = getElementCacheKey(newChild);
|
|
247
|
+
|
|
248
|
+
if (!oldCacheKey && !newCacheKey && (oldNode as any).__wsxManaged === true) {
|
|
249
|
+
const oldTag = (oldNode as HTMLElement | SVGElement).tagName.toLowerCase();
|
|
250
|
+
const newTag = newChild.tagName.toLowerCase();
|
|
251
|
+
if (oldTag === newTag && oldNode.textContent === newChild.textContent) {
|
|
252
|
+
// 内容相同,逻辑上旧节点已经“处理”过了
|
|
253
|
+
if (processedNodes) processedNodes.add(oldNode);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
221
258
|
// 关键修复:在替换元素之前,标记旧元素内的所有文本节点为已处理
|
|
222
|
-
// 这样可以防止在移除阶段被误删
|
|
223
|
-
// 注意:这里只标记直接子文本节点,不递归处理嵌套元素内的文本节点
|
|
224
|
-
// 因为 replaceChild 会一起处理元素及其所有子节点
|
|
225
259
|
parent.replaceChild(newChild, oldNode);
|
|
260
|
+
if (processedNodes) processedNodes.add(newChild);
|
|
261
|
+
} else {
|
|
262
|
+
// 已经是同一个节点
|
|
263
|
+
if (processedNodes) processedNodes.add(newChild);
|
|
226
264
|
}
|
|
227
265
|
} else {
|
|
228
|
-
//
|
|
229
|
-
// 如果 newChild 已经在 parent 中,不应该再次插入
|
|
230
|
-
// 如果 parent 中已经存在相同标签名和内容的元素,也不应该插入
|
|
231
|
-
// 这样可以防止重复插入相同的元素(特别是从 HTML 字符串解析而来的元素)
|
|
266
|
+
// ... 继续原有的插入逻辑 ...
|
|
232
267
|
if (newChild.parentNode === parent) {
|
|
233
|
-
//
|
|
268
|
+
// 已经在正确位置,不需要再次插入
|
|
234
269
|
return;
|
|
235
270
|
}
|
|
236
271
|
// RFC 0048 关键修复:检查是否已经存在相同内容的元素
|
|
@@ -257,6 +292,13 @@ export function replaceOrInsertElementAtPosition(
|
|
|
257
292
|
) {
|
|
258
293
|
// 找到相同内容的元素(且都没有 cache key),不需要插入 newChild
|
|
259
294
|
// 这是从 HTML 字符串解析而来的重复元素
|
|
295
|
+
// 关键修复:必须将其标记为已处理,否则会被 shouldRemoveNode 移除
|
|
296
|
+
console.log(
|
|
297
|
+
"[WSX Debug] Found duplicate content, keeping existing:",
|
|
298
|
+
existingNode.tagName,
|
|
299
|
+
existingNode.textContent
|
|
300
|
+
);
|
|
301
|
+
if (processedNodes) processedNodes.add(existingNode);
|
|
260
302
|
return;
|
|
261
303
|
}
|
|
262
304
|
}
|
|
@@ -281,6 +323,7 @@ export function appendNewChild(
|
|
|
281
323
|
|
|
282
324
|
if (typeof child === "string" || typeof child === "number") {
|
|
283
325
|
const newTextNode = document.createTextNode(String(child));
|
|
326
|
+
(newTextNode as any).__wsxManaged = true; // 标记为框架管理
|
|
284
327
|
parent.appendChild(newTextNode);
|
|
285
328
|
// 关键修复:标记新创建的文本节点为已处理,防止在移除阶段被误删
|
|
286
329
|
if (processedNodes) {
|
|
@@ -305,6 +348,13 @@ export function appendNewChild(
|
|
|
305
348
|
processedNodes.add(child);
|
|
306
349
|
}
|
|
307
350
|
} else if (child instanceof DocumentFragment) {
|
|
351
|
+
// 关键修复:记录 Fragment 中的所有子节点为已处理
|
|
352
|
+
// 否则它们会被后续的 collectNodesToRemove 误删
|
|
353
|
+
if (processedNodes) {
|
|
354
|
+
for (let i = 0; i < child.childNodes.length; i++) {
|
|
355
|
+
processedNodes.add(child.childNodes[i]);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
308
358
|
parent.appendChild(child);
|
|
309
359
|
}
|
|
310
360
|
}
|
|
@@ -347,32 +397,49 @@ export function shouldRemoveNode(
|
|
|
347
397
|
cacheKeyMap: Map<string, HTMLElement | SVGElement>,
|
|
348
398
|
processedNodes?: Set<Node>
|
|
349
399
|
): boolean {
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
400
|
+
// RFC 0048 & RFC 0053 关键修复:文本节点应该由框架管理,不应该被 shouldPreserveElement 保留
|
|
401
|
+
// 文本节点本身不是元素,shouldPreserveElement 对文本节点总是返回 true
|
|
402
|
+
// 但是,如果文本节点的父元素是由框架管理的,文本节点也应该由框架管理
|
|
403
|
+
// 只有当文本节点的父元素是保留元素时,文本节点才应该被保留
|
|
404
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
405
|
+
// 如果是框架管理的文本节点
|
|
406
|
+
if ((node as any).__wsxManaged === true) {
|
|
407
|
+
// 如果已被处理过,保留
|
|
408
|
+
if (processedNodes && processedNodes.has(node)) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
// 否则移除
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 如果是非框架管理的文本节点(第三方注入)
|
|
416
|
+
// 检查其父元素是否被保留
|
|
417
|
+
const parent = node.parentNode;
|
|
418
|
+
if (parent && (parent instanceof HTMLElement || parent instanceof SVGElement)) {
|
|
419
|
+
if (shouldPreserveElement(parent)) {
|
|
420
|
+
// 如果父元素是保留元素,文本节点也应该被保留
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// 对于未标记且父元素未保留的文本,默认移除以防止泄漏
|
|
425
|
+
return true;
|
|
426
|
+
} else {
|
|
427
|
+
// 对于元素节点,使用原有的逻辑
|
|
428
|
+
if (shouldPreserveElement(node)) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
353
431
|
}
|
|
354
432
|
|
|
355
|
-
|
|
356
|
-
|
|
433
|
+
const isProcessed = processedNodes && processedNodes.has(node);
|
|
434
|
+
|
|
435
|
+
if (isProcessed) {
|
|
357
436
|
return false;
|
|
358
437
|
}
|
|
359
438
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
// 但是,只有当父元素还在当前 parent 的子树中时才检查
|
|
363
|
-
if (node.nodeType === Node.TEXT_NODE && processedNodes) {
|
|
364
|
-
let parent = node.parentNode;
|
|
365
|
-
while (parent) {
|
|
366
|
-
// 关键修复:只有当父元素还在 DOM 中并且在当前 parent 的子树中时才检查
|
|
367
|
-
// 因为如果父元素被 replaceChild 移除了,它就不在 DOM 中了
|
|
368
|
-
if (processedNodes.has(parent) && parent.parentNode) {
|
|
369
|
-
return false; // 文本节点在已处理的元素内部,不应该移除
|
|
370
|
-
}
|
|
371
|
-
parent = parent.parentNode;
|
|
372
|
-
}
|
|
439
|
+
if (shouldPreserveElement(node)) {
|
|
440
|
+
return false;
|
|
373
441
|
}
|
|
374
442
|
|
|
375
|
-
// 检查是否在新子元素集合中(通过引用)
|
|
376
443
|
if (
|
|
377
444
|
node instanceof HTMLElement ||
|
|
378
445
|
node instanceof SVGElement ||
|
|
@@ -382,7 +449,6 @@ export function shouldRemoveNode(
|
|
|
382
449
|
return false;
|
|
383
450
|
}
|
|
384
451
|
|
|
385
|
-
// 检查是否通过 cache key 匹配
|
|
386
452
|
if (node instanceof HTMLElement || node instanceof SVGElement) {
|
|
387
453
|
const cacheKey = getElementCacheKey(node);
|
|
388
454
|
if (cacheKey && cacheKeyMap.has(cacheKey)) {
|
|
@@ -446,7 +512,8 @@ export function collectNodesToRemove(
|
|
|
446
512
|
|
|
447
513
|
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
448
514
|
const node = parent.childNodes[i];
|
|
449
|
-
|
|
515
|
+
const removed = shouldRemoveNode(node, elementSet, cacheKeyMap, processedNodes);
|
|
516
|
+
if (removed) {
|
|
450
517
|
nodesToRemove.push(node);
|
|
451
518
|
}
|
|
452
519
|
}
|
package/src/web-component.ts
CHANGED
|
@@ -225,9 +225,10 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
225
225
|
this.shadowRoot.appendChild(content);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
|
|
228
|
+
// 移除旧内容(保留样式元素、未标记元素和新渲染的内容)
|
|
229
|
+
// 关键修复 (RFC-0042): 使用 childNodes 而不是 children,确保文本节点也被清理
|
|
230
|
+
// 否则,在 Shadow DOM 根部的文本节点会发生泄漏
|
|
231
|
+
const oldNodes = Array.from(this.shadowRoot.childNodes).filter((child) => {
|
|
231
232
|
// 保留新添加的内容(或已经在 shadowRoot 中的 content)
|
|
232
233
|
if (child === content) {
|
|
233
234
|
return false;
|
|
@@ -237,13 +238,13 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
237
238
|
return false;
|
|
238
239
|
}
|
|
239
240
|
// 保留未标记的元素(第三方库注入的元素、自定义元素)
|
|
240
|
-
//
|
|
241
|
+
// 对于文本节点,shouldPreserveElement 逻辑已在 element-marking.ts 中优化
|
|
241
242
|
if (shouldPreserveElement(child)) {
|
|
242
243
|
return false;
|
|
243
244
|
}
|
|
244
245
|
return true;
|
|
245
246
|
});
|
|
246
|
-
|
|
247
|
+
oldNodes.forEach((node) => node.remove());
|
|
247
248
|
|
|
248
249
|
// 7. 恢复 adopted stylesheets(在 DOM 操作之后,确保样式不被意外移除)
|
|
249
250
|
// 关键修复:在 DOM 操作之后恢复样式,防止样式在 DOM 操作过程中被意外清空
|