@wsxjs/wsx-core 0.0.19 → 0.0.21
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 +906 -0
- package/dist/index.js +837 -98
- package/dist/index.mjs +137 -17
- package/dist/jsx-runtime.js +707 -86
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +707 -86
- package/dist/jsx.mjs +1 -1
- package/package.json +2 -2
- package/src/base-component.ts +15 -0
- package/src/dom-cache-manager.ts +135 -0
- package/src/jsx-factory.ts +73 -190
- package/src/light-component.ts +3 -2
- package/src/reactive-decorator.ts +9 -0
- package/src/render-context.ts +40 -0
- package/src/utils/cache-key.ts +114 -0
- package/src/utils/dom-utils.ts +119 -0
- package/src/utils/element-creation.ts +140 -0
- package/src/utils/element-marking.ts +80 -0
- package/src/utils/element-update.ts +377 -0
- package/src/utils/props-utils.ts +307 -0
- package/src/web-component.ts +2 -1
- package/dist/chunk-UH5BDYGI.mjs +0 -283
package/dist/jsx.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wsxjs/wsx-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"description": "Core WSXJS - Web Components with JSX syntax",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"custom-elements"
|
|
49
49
|
],
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@wsxjs/wsx-logger": "0.0.
|
|
51
|
+
"@wsxjs/wsx-logger": "0.0.21"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"tsup": "^8.0.0",
|
package/src/base-component.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { reactive as createReactive, createState, reactiveWithDebug } from "./utils/reactive";
|
|
12
|
+
import { DOMCacheManager } from "./dom-cache-manager";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Type for reactive state storage
|
|
@@ -60,6 +61,12 @@ export abstract class BaseComponent extends HTMLElement {
|
|
|
60
61
|
*/
|
|
61
62
|
protected _autoStyles?: string;
|
|
62
63
|
|
|
64
|
+
/**
|
|
65
|
+
* DOM Cache Manager for fine-grained updates (RFC 0037)
|
|
66
|
+
* @internal
|
|
67
|
+
*/
|
|
68
|
+
protected _domCache = new DOMCacheManager();
|
|
69
|
+
|
|
63
70
|
/**
|
|
64
71
|
* 当前捕获的焦点状态(用于在 render 时使用捕获的值)
|
|
65
72
|
* @internal - 由 rerender() 方法管理
|
|
@@ -138,6 +145,14 @@ export abstract class BaseComponent extends HTMLElement {
|
|
|
138
145
|
*/
|
|
139
146
|
protected onRendered?(): void;
|
|
140
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Gets the DOMCacheManager instance.
|
|
150
|
+
* @internal
|
|
151
|
+
*/
|
|
152
|
+
public getDomCache(): DOMCacheManager {
|
|
153
|
+
return this._domCache;
|
|
154
|
+
}
|
|
155
|
+
|
|
141
156
|
/**
|
|
142
157
|
* 处理 blur 事件,在用户停止输入时执行待处理的重渲染
|
|
143
158
|
* @internal
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { createLogger } from "./utils/logger";
|
|
2
|
+
|
|
3
|
+
const logger = createLogger("DOMCacheManager");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parent container information for duplicate key detection
|
|
7
|
+
*/
|
|
8
|
+
interface ParentInfo {
|
|
9
|
+
parentTag: string;
|
|
10
|
+
parentClass: string;
|
|
11
|
+
element: Element;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* DOMCacheManager
|
|
16
|
+
*
|
|
17
|
+
* Manages DOM element caching for fine-grained updates (RFC 0037).
|
|
18
|
+
* Stores elements by unique keys derived from component ID + position/key.
|
|
19
|
+
*/
|
|
20
|
+
export class DOMCacheManager {
|
|
21
|
+
// Map<CacheKey, DOMElement>
|
|
22
|
+
private cache = new Map<string, Element>();
|
|
23
|
+
|
|
24
|
+
// Map<DOMElement, Metadata>
|
|
25
|
+
// Stores metadata (props, children) for cached elements to support diffing
|
|
26
|
+
private metadata = new WeakMap<Element, Record<string, unknown>>();
|
|
27
|
+
|
|
28
|
+
// Track key-parent relationships to detect duplicate keys in all environments
|
|
29
|
+
// Map<CacheKey, ParentInfo>
|
|
30
|
+
private keyParentMap = new Map<string, ParentInfo>();
|
|
31
|
+
|
|
32
|
+
// Flag to enable duplicate key warnings (enabled by default, critical for correctness)
|
|
33
|
+
private warnDuplicateKeys = true;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Retrieves an element from the cache.
|
|
37
|
+
* @param key The unique cache key.
|
|
38
|
+
*/
|
|
39
|
+
get(key: string): Element | undefined {
|
|
40
|
+
return this.cache.get(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Stores an element in the cache.
|
|
45
|
+
* @param key The unique cache key.
|
|
46
|
+
* @param element The DOM element to cache.
|
|
47
|
+
*/
|
|
48
|
+
set(key: string, element: Element): void {
|
|
49
|
+
// Always check for duplicate keys (critical for correctness)
|
|
50
|
+
if (this.warnDuplicateKeys) {
|
|
51
|
+
this.checkDuplicateKey(key, element);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.cache.set(key, element);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Checks if a cache key is being reused in a different parent container.
|
|
59
|
+
* Runs in all environments to help developers catch key conflicts early.
|
|
60
|
+
* This is critical for correctness and helps prevent subtle bugs.
|
|
61
|
+
*/
|
|
62
|
+
private checkDuplicateKey(key: string, element: Element): void {
|
|
63
|
+
const existing = this.keyParentMap.get(key);
|
|
64
|
+
const currentParent = element.parentElement;
|
|
65
|
+
|
|
66
|
+
if (existing && currentParent) {
|
|
67
|
+
const currentParentInfo = this.getParentInfo(currentParent);
|
|
68
|
+
const existingParentInfo = `${existing.parentTag}${existing.parentClass ? "." + existing.parentClass : ""}`;
|
|
69
|
+
|
|
70
|
+
// Check if the element is being used in a different parent container
|
|
71
|
+
if (currentParentInfo !== existingParentInfo) {
|
|
72
|
+
logger.warn(
|
|
73
|
+
`Duplicate key "${key}" detected in different parent containers!\n` +
|
|
74
|
+
` Previous parent: ${existingParentInfo}\n` +
|
|
75
|
+
` Current parent: ${currentParentInfo}\n` +
|
|
76
|
+
`\n` +
|
|
77
|
+
`This may cause elements to appear in wrong containers or be moved unexpectedly.\n` +
|
|
78
|
+
`\n` +
|
|
79
|
+
`Solution: Use unique key prefixes for different locations:\n` +
|
|
80
|
+
` Example: <wsx-link key="nav-0"> vs <wsx-link key="overflow-0">\n` +
|
|
81
|
+
`\n` +
|
|
82
|
+
`See https://wsxjs.dev/docs/guide/DOM_CACHE_GUIDE for best practices.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Track this key-parent relationship for future checks
|
|
88
|
+
if (currentParent) {
|
|
89
|
+
this.keyParentMap.set(key, {
|
|
90
|
+
parentTag: currentParent.tagName.toLowerCase(),
|
|
91
|
+
parentClass: currentParent.className,
|
|
92
|
+
element,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Gets a formatted parent container description.
|
|
99
|
+
*/
|
|
100
|
+
private getParentInfo(parent: Element): string {
|
|
101
|
+
const tag = parent.tagName.toLowerCase();
|
|
102
|
+
const className = parent.className;
|
|
103
|
+
return `${tag}${className ? "." + className.split(" ")[0] : ""}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Checks if a key exists in the cache.
|
|
108
|
+
*/
|
|
109
|
+
has(key: string): boolean {
|
|
110
|
+
return this.cache.has(key);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Clears the cache.
|
|
115
|
+
* Should be called when component is disconnected or cache is invalidated.
|
|
116
|
+
*/
|
|
117
|
+
clear(): void {
|
|
118
|
+
this.cache.clear();
|
|
119
|
+
// WeakMap doesn't need clearing
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Stores metadata for an element (e.g. previous props).
|
|
124
|
+
*/
|
|
125
|
+
setMetadata(element: Element, meta: Record<string, unknown>): void {
|
|
126
|
+
this.metadata.set(element, meta);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Retrieves metadata for an element.
|
|
131
|
+
*/
|
|
132
|
+
getMetadata(element: Element): Record<string, unknown> | undefined {
|
|
133
|
+
return this.metadata.get(element);
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/jsx-factory.ts
CHANGED
|
@@ -11,20 +11,18 @@
|
|
|
11
11
|
|
|
12
12
|
// JSX 类型声明已移至 types/wsx-types.d.ts
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
| undefined
|
|
27
|
-
| boolean;
|
|
14
|
+
import { flattenChildren, type JSXChildren } from "./utils/dom-utils";
|
|
15
|
+
import { generateCacheKey, getComponentId } from "./utils/cache-key";
|
|
16
|
+
import { markElement } from "./utils/element-marking";
|
|
17
|
+
import { RenderContext } from "./render-context";
|
|
18
|
+
import { createElementWithPropsAndChildren } from "./utils/element-creation";
|
|
19
|
+
import { updateElement } from "./utils/element-update";
|
|
20
|
+
import type { BaseComponent } from "./base-component";
|
|
21
|
+
import type { DOMCacheManager } from "./dom-cache-manager";
|
|
22
|
+
import { createLogger } from "./utils/logger";
|
|
23
|
+
const logger = createLogger("JSX Factory");
|
|
24
|
+
// JSX子元素类型(从 dom-utils 重新导出以保持向后兼容)
|
|
25
|
+
export type { JSXChildren } from "./utils/dom-utils";
|
|
28
26
|
|
|
29
27
|
/**
|
|
30
28
|
* 纯原生JSX工厂函数
|
|
@@ -44,200 +42,85 @@ export function h(
|
|
|
44
42
|
props: Record<string, unknown> | null = {},
|
|
45
43
|
...children: JSXChildren[]
|
|
46
44
|
): HTMLElement | SVGElement {
|
|
47
|
-
//
|
|
45
|
+
// 处理组件函数(不受缓存影响)
|
|
48
46
|
if (typeof tag === "function") {
|
|
49
47
|
return tag(props, children);
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
// 处理属性
|
|
56
|
-
if (props) {
|
|
57
|
-
const isSVG = shouldUseSVGNamespace(tag);
|
|
50
|
+
// 检查上下文(阶段 3.2:启用缓存机制)
|
|
51
|
+
const context = RenderContext.getCurrentComponent();
|
|
52
|
+
const cacheManager = context ? RenderContext.getDOMCache() : null;
|
|
58
53
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// 处理ref回调
|
|
65
|
-
if (key === "ref" && typeof value === "function") {
|
|
66
|
-
value(element);
|
|
67
|
-
}
|
|
68
|
-
// 处理className和class
|
|
69
|
-
else if (key === "className" || key === "class") {
|
|
70
|
-
if (isSVG) {
|
|
71
|
-
// SVG元素使用class属性
|
|
72
|
-
element.setAttribute("class", value as string);
|
|
73
|
-
} else {
|
|
74
|
-
// HTML元素可以使用className
|
|
75
|
-
(element as HTMLElement).className = value as string;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
// 处理style
|
|
79
|
-
else if (key === "style" && typeof value === "string") {
|
|
80
|
-
element.setAttribute("style", value);
|
|
81
|
-
}
|
|
82
|
-
// 处理事件监听器
|
|
83
|
-
else if (key.startsWith("on") && typeof value === "function") {
|
|
84
|
-
const eventName = key.slice(2).toLowerCase();
|
|
85
|
-
element.addEventListener(eventName, value as EventListener);
|
|
86
|
-
}
|
|
87
|
-
// 处理布尔属性
|
|
88
|
-
else if (typeof value === "boolean") {
|
|
89
|
-
if (value) {
|
|
90
|
-
element.setAttribute(key, "");
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// 特殊处理 input/textarea/select 的 value 属性
|
|
94
|
-
// 使用 .value 而不是 setAttribute,因为 .value 是当前值,setAttribute 是初始值
|
|
95
|
-
else if (key === "value") {
|
|
96
|
-
if (
|
|
97
|
-
element instanceof HTMLInputElement ||
|
|
98
|
-
element instanceof HTMLTextAreaElement ||
|
|
99
|
-
element instanceof HTMLSelectElement
|
|
100
|
-
) {
|
|
101
|
-
element.value = String(value);
|
|
102
|
-
} else {
|
|
103
|
-
// 对于其他元素,使用 setAttribute
|
|
104
|
-
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
105
|
-
element.setAttribute(attributeName, String(value));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// 处理其他属性
|
|
109
|
-
else {
|
|
110
|
-
// 对SVG元素使用正确的属性名
|
|
111
|
-
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
112
|
-
element.setAttribute(attributeName, String(value));
|
|
113
|
-
}
|
|
114
|
-
});
|
|
54
|
+
if (context && cacheManager) {
|
|
55
|
+
return tryUseCacheOrCreate(tag, props, children, context, cacheManager);
|
|
115
56
|
}
|
|
116
57
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
flatChildren.forEach((child) => {
|
|
120
|
-
if (child === null || child === undefined || child === false) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (typeof child === "string" || typeof child === "number") {
|
|
125
|
-
element.appendChild(document.createTextNode(String(child)));
|
|
126
|
-
} else if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
127
|
-
element.appendChild(child);
|
|
128
|
-
} else if (child instanceof DocumentFragment) {
|
|
129
|
-
element.appendChild(child);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
return element;
|
|
58
|
+
// 无上下文:使用旧逻辑(向后兼容)
|
|
59
|
+
return createElementWithPropsAndChildren(tag, props, children);
|
|
134
60
|
}
|
|
135
61
|
|
|
136
62
|
/**
|
|
137
|
-
*
|
|
138
|
-
* 使用更严格的检测:必须包含完整的 HTML 标签(开始和结束标签,或自闭合标签)
|
|
63
|
+
* Tries to use cached element or creates a new one.
|
|
139
64
|
*/
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
65
|
+
function tryUseCacheOrCreate(
|
|
66
|
+
tag: string,
|
|
67
|
+
props: Record<string, unknown> | null,
|
|
68
|
+
children: JSXChildren[],
|
|
69
|
+
context: BaseComponent,
|
|
70
|
+
cacheManager: DOMCacheManager
|
|
71
|
+
): HTMLElement | SVGElement {
|
|
72
|
+
try {
|
|
73
|
+
const componentId = getComponentId();
|
|
74
|
+
const cacheKey = generateCacheKey(tag, props, componentId, context);
|
|
75
|
+
const cachedElement = cacheManager.get(cacheKey);
|
|
76
|
+
|
|
77
|
+
if (cachedElement) {
|
|
78
|
+
// ✅ 缓存命中:复用元素并更新内容(阶段 4:细粒度更新)
|
|
79
|
+
// 缓存键已经确保了唯一性(componentId + tag + position/key/index)
|
|
80
|
+
// 不需要再检查标签名(可能导致错误复用)
|
|
81
|
+
const element = cachedElement as HTMLElement | SVGElement;
|
|
82
|
+
updateElement(element, props, children, tag, cacheManager);
|
|
83
|
+
return element;
|
|
84
|
+
}
|
|
155
85
|
|
|
156
|
-
|
|
86
|
+
// ❌ 缓存未命中:创建新元素
|
|
87
|
+
const element = createElementWithPropsAndChildren(tag, props, children);
|
|
88
|
+
cacheManager.set(cacheKey, element);
|
|
89
|
+
markElement(element, cacheKey);
|
|
90
|
+
// 保存初始元数据(用于下次更新)
|
|
91
|
+
cacheManager.setMetadata(element, {
|
|
92
|
+
props: props || {},
|
|
93
|
+
children: children,
|
|
94
|
+
});
|
|
95
|
+
return element;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// 缓存失败:降级到创建新元素
|
|
98
|
+
return handleCacheError(error, tag, props, children);
|
|
99
|
+
}
|
|
157
100
|
}
|
|
158
101
|
|
|
159
102
|
/**
|
|
160
|
-
*
|
|
161
|
-
* 自动检测HTML字符串并转换为DOM节点
|
|
162
|
-
*
|
|
163
|
-
* @param children - 子元素数组
|
|
164
|
-
* @param skipHTMLDetection - 是否跳过HTML检测(用于已解析的节点,避免无限递归)
|
|
165
|
-
* @param depth - 当前递归深度(防止无限递归,最大深度为 10)
|
|
103
|
+
* Handles cache errors by logging and falling back to creating a new element.
|
|
166
104
|
*/
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
const result: (
|
|
183
|
-
| string
|
|
184
|
-
| number
|
|
185
|
-
| HTMLElement
|
|
186
|
-
| SVGElement
|
|
187
|
-
| DocumentFragment
|
|
188
|
-
| boolean
|
|
189
|
-
| null
|
|
190
|
-
| undefined
|
|
191
|
-
)[] = [];
|
|
192
|
-
|
|
193
|
-
for (const child of children) {
|
|
194
|
-
if (child === null || child === undefined || child === false) {
|
|
195
|
-
continue;
|
|
196
|
-
} else if (Array.isArray(child)) {
|
|
197
|
-
// 递归处理数组,保持 skipHTMLDetection 状态,增加深度
|
|
198
|
-
result.push(...flattenChildren(child, skipHTMLDetection, depth + 1));
|
|
199
|
-
} else if (typeof child === "string") {
|
|
200
|
-
// 如果跳过HTML检测,直接添加字符串(避免无限递归)
|
|
201
|
-
if (skipHTMLDetection) {
|
|
202
|
-
result.push(child);
|
|
203
|
-
} else if (isHTMLString(child)) {
|
|
204
|
-
// 自动检测HTML字符串并转换为DOM节点
|
|
205
|
-
// 使用 try-catch 防止解析失败导致崩溃
|
|
206
|
-
try {
|
|
207
|
-
const nodes = parseHTMLToNodes(child);
|
|
208
|
-
// 递归处理转换后的节点数组,标记为已解析,避免再次检测HTML
|
|
209
|
-
// parseHTMLToNodes 返回的字符串是纯文本节点,不应该再次被检测为HTML
|
|
210
|
-
// 但是为了安全,我们仍然设置 skipHTMLDetection = true
|
|
211
|
-
if (nodes.length > 0) {
|
|
212
|
-
// 直接添加解析后的节点,不再递归处理(避免无限递归)
|
|
213
|
-
// parseHTMLToNodes 已经完成了所有解析工作
|
|
214
|
-
for (const node of nodes) {
|
|
215
|
-
if (typeof node === "string") {
|
|
216
|
-
// 文本节点直接添加,不再检测 HTML(已解析)
|
|
217
|
-
result.push(node);
|
|
218
|
-
} else {
|
|
219
|
-
// DOM 元素直接添加
|
|
220
|
-
result.push(node);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
} else {
|
|
224
|
-
// 如果解析失败,回退到纯文本
|
|
225
|
-
result.push(child);
|
|
226
|
-
}
|
|
227
|
-
} catch (error) {
|
|
228
|
-
// 如果解析失败,回退到纯文本,避免崩溃
|
|
229
|
-
console.warn("[WSX] Failed to parse HTML string, treating as text:", error);
|
|
230
|
-
result.push(child);
|
|
231
|
-
}
|
|
232
|
-
} else {
|
|
233
|
-
result.push(child);
|
|
234
|
-
}
|
|
235
|
-
} else {
|
|
236
|
-
result.push(child);
|
|
105
|
+
function handleCacheError(
|
|
106
|
+
error: unknown,
|
|
107
|
+
tag: string,
|
|
108
|
+
props: Record<string, unknown> | null,
|
|
109
|
+
children: JSXChildren[]
|
|
110
|
+
): HTMLElement | SVGElement {
|
|
111
|
+
// 在开发环境输出警告,帮助调试
|
|
112
|
+
try {
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
+
const nodeEnv = (typeof (globalThis as any).process !== "undefined" &&
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
(globalThis as any).process.env?.NODE_ENV) as string | undefined;
|
|
117
|
+
if (nodeEnv === "development") {
|
|
118
|
+
logger.warn("[WSX DOM Cache] Cache error, falling back to create new element:", error);
|
|
237
119
|
}
|
|
120
|
+
} catch {
|
|
121
|
+
// 忽略环境变量检查错误
|
|
238
122
|
}
|
|
239
|
-
|
|
240
|
-
return result;
|
|
123
|
+
return createElementWithPropsAndChildren(tag, props, children);
|
|
241
124
|
}
|
|
242
125
|
|
|
243
126
|
/**
|
package/src/light-component.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { h, type JSXChildren } from "./jsx-factory";
|
|
11
11
|
import { BaseComponent, type BaseComponentConfig } from "./base-component";
|
|
12
|
+
import { RenderContext } from "./render-context";
|
|
12
13
|
import { createLogger } from "@wsxjs/wsx-logger";
|
|
13
14
|
|
|
14
15
|
const logger = createLogger("LightComponent");
|
|
@@ -95,7 +96,7 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
95
96
|
childrenToRemove.forEach((child) => child.remove());
|
|
96
97
|
|
|
97
98
|
// 渲染JSX内容到Light DOM
|
|
98
|
-
const content = this.render();
|
|
99
|
+
const content = RenderContext.runInContext(this, () => this.render());
|
|
99
100
|
this.appendChild(content);
|
|
100
101
|
|
|
101
102
|
// 确保样式元素在第一个位置(如果存在)
|
|
@@ -178,7 +179,7 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
178
179
|
|
|
179
180
|
try {
|
|
180
181
|
// 3. 重新渲染JSX内容
|
|
181
|
-
const content = this.render();
|
|
182
|
+
const content = RenderContext.runInContext(this, () => this.render());
|
|
182
183
|
|
|
183
184
|
// 4. 在添加到 DOM 之前恢复值,避免浏览器渲染状态值
|
|
184
185
|
if (focusState && focusState.key && focusState.value !== undefined) {
|
|
@@ -78,6 +78,15 @@ export function state(
|
|
|
78
78
|
// Compatibility with Babel plugin which is required for this decorator to work properly
|
|
79
79
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
80
|
): any {
|
|
81
|
+
// RFC 0037 Phase 0: Test Infrastructure Support
|
|
82
|
+
// Allow runtime decorator in tests to bypass Babel plugin requirement
|
|
83
|
+
// Use globalThis to safely access process in all environments
|
|
84
|
+
|
|
85
|
+
const globalProcess =
|
|
86
|
+
typeof globalThis !== "undefined" ? (globalThis as any).process : undefined;
|
|
87
|
+
if (globalProcess?.env?.NODE_ENV === "test") {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
81
90
|
/**
|
|
82
91
|
* @state decorator MUST be processed by Babel plugin at compile time.
|
|
83
92
|
* If this function is executed at runtime, it means Babel plugin did not process it.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { BaseComponent } from "./base-component";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RenderContext
|
|
5
|
+
*
|
|
6
|
+
* Tracks the currently rendering component instance.
|
|
7
|
+
* improved for RFC 0037 to support DOM caching and optimization.
|
|
8
|
+
*/
|
|
9
|
+
export class RenderContext {
|
|
10
|
+
private static current: BaseComponent | null = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Executes a function within the context of a component.
|
|
14
|
+
* @param component The component instance currently rendering.
|
|
15
|
+
* @param fn The function to execute (usually the render method).
|
|
16
|
+
*/
|
|
17
|
+
static runInContext<T>(component: BaseComponent, fn: () => T): T {
|
|
18
|
+
const prev = RenderContext.current;
|
|
19
|
+
RenderContext.current = component;
|
|
20
|
+
try {
|
|
21
|
+
return fn();
|
|
22
|
+
} finally {
|
|
23
|
+
RenderContext.current = prev;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets the currently rendering component.
|
|
29
|
+
*/
|
|
30
|
+
static getCurrentComponent(): BaseComponent | null {
|
|
31
|
+
return RenderContext.current;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Gets the current component's DOM cache.
|
|
36
|
+
*/
|
|
37
|
+
static getDOMCache() {
|
|
38
|
+
return RenderContext.current?.getDomCache();
|
|
39
|
+
}
|
|
40
|
+
}
|