@wsxjs/wsx-core 0.0.7 → 0.0.9

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.
@@ -49,7 +49,7 @@ class UpdateScheduler {
49
49
  try {
50
50
  callback();
51
51
  } catch (error) {
52
- console.error("[WSX Reactive] Error in callback:", error);
52
+ logger.error("[WSX Reactive] Error in callback:", error);
53
53
  }
54
54
  });
55
55
  }
@@ -58,6 +58,82 @@ class UpdateScheduler {
58
58
  // 全局调度器实例
59
59
  const scheduler = new UpdateScheduler();
60
60
 
61
+ // Proxy 缓存:避免为同一个对象创建多个 Proxy
62
+ // 使用 WeakMap 确保对象被垃圾回收时,对应的 Proxy 也被清理
63
+ // Key: 原始对象, Value: Proxy
64
+ const proxyCache = new WeakMap<object, unknown>();
65
+
66
+ // 反向映射:从 Proxy 到原始对象
67
+ // 用于在 set trap 中比较原始对象而不是 Proxy
68
+ const originalCache = new WeakMap<object, object>();
69
+
70
+ /**
71
+ * 递归展开 Proxy,返回完全干净的对象(不包含任何 Proxy)
72
+ * 用于 JSON.stringify 等需要序列化的场景
73
+ * 使用 WeakSet 防止循环引用导致的无限递归
74
+ */
75
+ const unwrappingSet = new WeakSet<object>();
76
+
77
+ function unwrapProxy(value: unknown): unknown {
78
+ if (value == null || typeof value !== "object") {
79
+ return value;
80
+ }
81
+
82
+ // 如果是 Proxy,获取原始对象
83
+ let original = value;
84
+ if (originalCache.has(value)) {
85
+ original = originalCache.get(value)!;
86
+ }
87
+
88
+ // 防止循环引用
89
+ if (unwrappingSet.has(original)) {
90
+ return null; // 循环引用时返回 null
91
+ }
92
+
93
+ unwrappingSet.add(original);
94
+
95
+ try {
96
+ if (Array.isArray(original)) {
97
+ return original.map((item) => unwrapProxy(item));
98
+ }
99
+
100
+ const result: Record<string, unknown> = {};
101
+ // 直接访问原始对象的属性,不通过 Proxy
102
+ for (const key in original) {
103
+ if (Object.prototype.hasOwnProperty.call(original, key)) {
104
+ const propValue = original[key];
105
+ // 如果属性值是 Proxy,先获取原始对象再递归
106
+ if (
107
+ propValue != null &&
108
+ typeof propValue === "object" &&
109
+ originalCache.has(propValue)
110
+ ) {
111
+ result[key] = unwrapProxy(originalCache.get(propValue)!);
112
+ } else {
113
+ result[key] = unwrapProxy(propValue);
114
+ }
115
+ }
116
+ }
117
+
118
+ return result;
119
+ } finally {
120
+ unwrappingSet.delete(original);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * 数组变异方法列表 - 这些方法会修改数组内容
126
+ */
127
+ const ARRAY_MUTATION_METHODS = [
128
+ "push",
129
+ "pop",
130
+ "shift",
131
+ "unshift",
132
+ "splice",
133
+ "sort",
134
+ "reverse",
135
+ ] as const;
136
+
61
137
  /**
62
138
  * 创建响应式对象
63
139
  *
@@ -66,13 +142,34 @@ const scheduler = new UpdateScheduler();
66
142
  * @returns 响应式代理对象
67
143
  */
68
144
  export function reactive<T extends object>(obj: T, onChange: ReactiveCallback): T {
69
- return new Proxy(obj, {
70
- set(target: T, key: string | symbol, value: any): boolean {
145
+ // 检查缓存,避免为同一个对象创建多个 Proxy
146
+ if (proxyCache.has(obj)) {
147
+ return proxyCache.get(obj) as T;
148
+ }
149
+
150
+ // 检查是否为数组
151
+ const isArray = Array.isArray(obj);
152
+
153
+ const proxy = new Proxy(obj, {
154
+ set(target: T, key: string | symbol, value: unknown): boolean {
71
155
  const oldValue = target[key as keyof T];
72
156
 
73
- // 只有值真正改变时才触发更新
74
- if (oldValue !== value) {
75
- target[key as keyof T] = value;
157
+ // 获取原始对象进行比较(如果 oldValue 是 Proxy)
158
+ const oldOriginal = originalCache.get(oldValue as object) || oldValue;
159
+ const newOriginal =
160
+ value != null && typeof value === "object"
161
+ ? originalCache.get(value as object) || value
162
+ : value;
163
+
164
+ // 只有值真正改变时才触发更新(比较原始对象)
165
+ if (oldOriginal !== newOriginal) {
166
+ // 如果新值是对象或数组,确保它也被包装为响应式
167
+ if (value != null && typeof value === "object") {
168
+ const reactiveValue = reactive(value as object, onChange);
169
+ target[key as keyof T] = reactiveValue as T[keyof T];
170
+ } else {
171
+ target[key as keyof T] = value as T[keyof T];
172
+ }
76
173
 
77
174
  // 调度更新
78
175
  scheduler.schedule(onChange);
@@ -81,22 +178,62 @@ export function reactive<T extends object>(obj: T, onChange: ReactiveCallback):
81
178
  return true;
82
179
  },
83
180
 
84
- get(target: T, key: string | symbol): any {
85
- return target[key as keyof T];
181
+ get(target: T, key: string | symbol): unknown {
182
+ // 支持 toJSON,让 JSON.stringify 使用原始对象,避免触发 Proxy trap 递归
183
+ if (key === "toJSON") {
184
+ return function () {
185
+ // 递归展开所有嵌套 Proxy,返回完全干净的对象
186
+ return unwrapProxy(obj);
187
+ };
188
+ }
189
+
190
+ const value = target[key as keyof T];
191
+
192
+ // 如果是数组,拦截数组变异方法
193
+ if (
194
+ isArray &&
195
+ typeof key === "string" &&
196
+ ARRAY_MUTATION_METHODS.includes(key as (typeof ARRAY_MUTATION_METHODS)[number])
197
+ ) {
198
+ return function (this: unknown, ...args: unknown[]) {
199
+ // 调用原始方法
200
+ const arrayMethod = Array.prototype[key as keyof Array<unknown>] as (
201
+ ...args: unknown[]
202
+ ) => unknown;
203
+ const result = arrayMethod.apply(target, args);
204
+
205
+ // 数组内容已改变,触发更新
206
+ scheduler.schedule(onChange);
207
+
208
+ return result;
209
+ };
210
+ }
211
+
212
+ // 如果值是对象或数组,自动包装为响应式(支持嵌套对象)
213
+ if (value != null && typeof value === "object") {
214
+ // 先检查缓存,避免重复创建 Proxy
215
+ // 如果对象已经在缓存中,直接返回缓存的 Proxy(使用相同的 onChange)
216
+ if (proxyCache.has(value)) {
217
+ return proxyCache.get(value);
218
+ }
219
+ // 否则创建新的 Proxy
220
+ return reactive(value, onChange);
221
+ }
222
+
223
+ return value;
86
224
  },
87
225
 
88
226
  has(target: T, key: string | symbol): boolean {
89
227
  return key in target;
90
228
  },
229
+ });
91
230
 
92
- ownKeys(target: T): ArrayLike<string | symbol> {
93
- return Reflect.ownKeys(target);
94
- },
231
+ // 缓存 Proxy,避免重复创建
232
+ proxyCache.set(obj, proxy);
233
+ // 缓存反向映射:从 Proxy 到原始对象
234
+ originalCache.set(proxy, obj);
95
235
 
96
- getOwnPropertyDescriptor(target: T, key: string | symbol): PropertyDescriptor | undefined {
97
- return Reflect.getOwnPropertyDescriptor(target, key);
98
- },
99
- });
236
+ return proxy;
100
237
  }
101
238
 
102
239
  /**
@@ -128,18 +265,6 @@ export function createState<T>(
128
265
  return [getter, setter];
129
266
  }
130
267
 
131
- /**
132
- * 检查一个值是否为响应式对象
133
- */
134
- export function isReactive(value: any): boolean {
135
- return (
136
- value != null &&
137
- typeof value === "object" &&
138
- value.constructor === Object &&
139
- typeof value.valueOf === "function"
140
- );
141
- }
142
-
143
268
  /**
144
269
  * 开发模式下的调试工具
145
270
  */
@@ -149,7 +274,7 @@ export const ReactiveDebug = {
149
274
  */
150
275
  enable(): void {
151
276
  if (typeof window !== "undefined") {
152
- (window as any).__WSX_REACTIVE_DEBUG__ = true;
277
+ (window as Window & { __WSX_REACTIVE_DEBUG__?: boolean }).__WSX_REACTIVE_DEBUG__ = true;
153
278
  }
154
279
  },
155
280
 
@@ -158,7 +283,8 @@ export const ReactiveDebug = {
158
283
  */
159
284
  disable(): void {
160
285
  if (typeof window !== "undefined") {
161
- (window as any).__WSX_REACTIVE_DEBUG__ = false;
286
+ (window as Window & { __WSX_REACTIVE_DEBUG__?: boolean }).__WSX_REACTIVE_DEBUG__ =
287
+ false;
162
288
  }
163
289
  },
164
290
 
@@ -166,13 +292,17 @@ export const ReactiveDebug = {
166
292
  * 检查是否启用调试模式
167
293
  */
168
294
  isEnabled(): boolean {
169
- return typeof window !== "undefined" && (window as any).__WSX_REACTIVE_DEBUG__ === true;
295
+ return (
296
+ typeof window !== "undefined" &&
297
+ (window as Window & { __WSX_REACTIVE_DEBUG__?: boolean }).__WSX_REACTIVE_DEBUG__ ===
298
+ true
299
+ );
170
300
  },
171
301
 
172
302
  /**
173
303
  * 调试日志
174
304
  */
175
- log(message: string, ...args: any[]): void {
305
+ log(message: string, ...args: unknown[]): void {
176
306
  if (this.isEnabled()) {
177
307
  logger.info(`[WSX Reactive] ${message}`, ...args);
178
308
  }
@@ -188,9 +318,10 @@ export function reactiveWithDebug<T extends object>(
188
318
  debugName?: string
189
319
  ): T {
190
320
  const name = debugName || obj.constructor.name || "Unknown";
321
+ const isArray = Array.isArray(obj);
191
322
 
192
323
  return new Proxy(obj, {
193
- set(target: T, key: string | symbol, value: any): boolean {
324
+ set(target: T, key: string | symbol, value: unknown): boolean {
194
325
  const oldValue = target[key as keyof T];
195
326
 
196
327
  if (oldValue !== value) {
@@ -200,15 +331,58 @@ export function reactiveWithDebug<T extends object>(
200
331
  newValue: value,
201
332
  });
202
333
 
203
- target[key as keyof T] = value;
334
+ target[key as keyof T] = value as T[keyof T];
204
335
  scheduler.schedule(onChange);
205
336
  }
206
337
 
207
338
  return true;
208
339
  },
209
340
 
210
- get(target: T, key: string | symbol): any {
211
- return target[key as keyof T];
341
+ get(target: T, key: string | symbol): unknown {
342
+ // 支持 toJSON,让 JSON.stringify 使用原始对象,避免触发 Proxy trap 递归
343
+ if (key === "toJSON") {
344
+ return function () {
345
+ // 递归展开所有嵌套 Proxy,返回完全干净的对象
346
+ return unwrapProxy(obj);
347
+ };
348
+ }
349
+
350
+ const value = target[key as keyof T];
351
+
352
+ // 如果是数组,拦截数组变异方法
353
+ if (
354
+ isArray &&
355
+ typeof key === "string" &&
356
+ ARRAY_MUTATION_METHODS.includes(key as (typeof ARRAY_MUTATION_METHODS)[number])
357
+ ) {
358
+ return function (this: unknown, ...args: unknown[]) {
359
+ ReactiveDebug.log(`Array mutation in ${name}:`, {
360
+ method: key,
361
+ args,
362
+ });
363
+
364
+ // 调用原始方法
365
+ const arrayMethod = Array.prototype[key as keyof Array<unknown>] as (
366
+ ...args: unknown[]
367
+ ) => unknown;
368
+ const result = arrayMethod.apply(target, args);
369
+
370
+ // 数组内容已改变,触发更新
371
+ scheduler.schedule(onChange);
372
+
373
+ return result;
374
+ };
375
+ }
376
+
377
+ // 如果值是对象或数组,自动包装为响应式(支持嵌套对象)
378
+ if (value != null && typeof value === "object") {
379
+ // 检查是否已经是响应式(通过检查是否有 Proxy 标记)
380
+ // 简单检查:如果值已经是 Proxy,直接返回
381
+ // 否则,递归包装为响应式
382
+ return reactiveWithDebug(value, onChange, `${name}.${String(key)}`);
383
+ }
384
+
385
+ return value;
212
386
  },
213
387
  });
214
388
  }
@@ -10,43 +10,29 @@
10
10
 
11
11
  import { h, type JSXChildren } from "./jsx-factory";
12
12
  import { StyleManager } from "./styles/style-manager";
13
+ import { BaseComponent, type BaseComponentConfig } from "./base-component";
14
+ import { createLogger } from "./utils/logger";
15
+
16
+ const logger = createLogger("WebComponent");
13
17
 
14
18
  /**
15
19
  * Web Component 配置接口
16
20
  */
17
- export interface WebComponentConfig {
18
- styles?: string; // CSS内容
19
- styleName?: string; // 样式名称,用于缓存
20
- [key: string]: unknown;
21
- }
21
+ export type WebComponentConfig = BaseComponentConfig;
22
22
 
23
23
  /**
24
24
  * 通用 WSX Web Component 基础抽象类
25
25
  */
26
- export abstract class WebComponent extends HTMLElement {
26
+ export abstract class WebComponent extends BaseComponent {
27
27
  declare shadowRoot: ShadowRoot;
28
- protected config: WebComponentConfig;
29
- protected connected: boolean = false;
30
-
31
- /**
32
- * 子类应该重写这个方法来定义观察的属性
33
- * @returns 要观察的属性名数组
34
- */
35
- static get observedAttributes(): string[] {
36
- return [];
37
- }
28
+ protected config!: WebComponentConfig; // Initialized by BaseComponent constructor
38
29
 
39
30
  constructor(config: WebComponentConfig = {}) {
40
- super();
41
-
42
- this.config = config;
31
+ super(config);
32
+ // BaseComponent already created this.config with getter for styles
43
33
  this.attachShadow({ mode: "open" });
44
-
45
- // 自动应用CSS样式
46
- if (config.styles) {
47
- const styleName = config.styleName || this.constructor.name;
48
- StyleManager.applyStyles(this.shadowRoot, styleName, config.styles);
49
- }
34
+ // Styles are applied in connectedCallback for consistency with LightComponent
35
+ // and to ensure all class properties (including getters) are initialized
50
36
  }
51
37
 
52
38
  /**
@@ -62,14 +48,25 @@ export abstract class WebComponent extends HTMLElement {
62
48
  connectedCallback(): void {
63
49
  this.connected = true;
64
50
  try {
51
+ // 应用CSS样式到Shadow DOM
52
+ // Check both _autoStyles getter and config.styles getter
53
+ const stylesToApply = this._autoStyles || this.config.styles;
54
+ if (stylesToApply) {
55
+ const styleName = this.config.styleName || this.constructor.name;
56
+ StyleManager.applyStyles(this.shadowRoot, styleName, stylesToApply);
57
+ }
58
+
65
59
  // 渲染JSX内容到Shadow DOM
66
60
  const content = this.render();
67
61
  this.shadowRoot.appendChild(content);
68
62
 
63
+ // 初始化事件监听器
64
+ this.initializeEventListeners();
65
+
69
66
  // 调用子类的初始化钩子
70
67
  this.onConnected?.();
71
68
  } catch (error) {
72
- console.error(`[${this.constructor.name}] Error in connectedCallback:`, error);
69
+ logger.error(`Error in connectedCallback:`, error);
73
70
  this.renderError(error);
74
71
  }
75
72
  }
@@ -78,31 +75,11 @@ export abstract class WebComponent extends HTMLElement {
78
75
  * Web Component生命周期:从DOM断开
79
76
  */
80
77
  disconnectedCallback(): void {
78
+ this.connected = false;
79
+ this.cleanup(); // 清理资源(包括防抖定时器)
81
80
  this.onDisconnected?.();
82
81
  }
83
82
 
84
- /**
85
- * Web Component生命周期:属性变化
86
- */
87
- attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
88
- this.onAttributeChanged?.(name, oldValue, newValue);
89
- }
90
-
91
- /**
92
- * 可选生命周期钩子:组件已连接
93
- */
94
- protected onConnected?(): void;
95
-
96
- /**
97
- * 可选生命周期钩子:组件已断开
98
- */
99
- protected onDisconnected?(): void;
100
-
101
- /**
102
- * 可选生命周期钩子:属性已更改
103
- */
104
- protected onAttributeChanged?(name: string, oldValue: string, newValue: string): void;
105
-
106
83
  /**
107
84
  * 查找Shadow DOM内的元素
108
85
  *
@@ -128,34 +105,78 @@ export abstract class WebComponent extends HTMLElement {
128
105
  */
129
106
  protected rerender(): void {
130
107
  if (!this.connected) {
131
- console.warn(
132
- `[${this.constructor.name}] Component is not connected, skipping rerender.`
133
- );
108
+ logger.warn("Component is not connected, skipping rerender.");
134
109
  return;
135
110
  }
136
- // 保存当前的 adopted stylesheets (jsdom may not support this)
137
- const adoptedStyleSheets = this.shadowRoot.adoptedStyleSheets || [];
138
-
139
- // 清空现有内容但保留样式
140
- this.shadowRoot.innerHTML = "";
141
111
 
142
- // 恢复 adopted stylesheets (避免重新应用样式)
143
- if (this.shadowRoot.adoptedStyleSheets) {
144
- this.shadowRoot.adoptedStyleSheets = adoptedStyleSheets;
145
- }
112
+ // 1. 捕获焦点状态(在 DOM 替换之前)
113
+ const focusState = this.captureFocusState();
114
+ // 保存到实例变量,供 render() 使用(如果需要)
115
+ this._pendingFocusState = focusState;
146
116
 
147
- // 只有在没有 adopted stylesheets 时才重新应用样式
148
- if (adoptedStyleSheets.length === 0 && this.config.styles) {
149
- const styleName = this.config.styleName || this.constructor.name;
150
- StyleManager.applyStyles(this.shadowRoot, styleName, this.config.styles);
151
- }
117
+ // 保存当前的 adopted stylesheets (jsdom may not support this)
118
+ const adoptedStyleSheets = this.shadowRoot.adoptedStyleSheets || [];
152
119
 
153
- // 重新渲染JSX
154
120
  try {
121
+ // 只有在没有 adopted stylesheets 时才重新应用样式
122
+ // Check both _autoStyles getter and config.styles getter
123
+ if (adoptedStyleSheets.length === 0) {
124
+ const stylesToApply = this._autoStyles || this.config.styles;
125
+ if (stylesToApply) {
126
+ const styleName = this.config.styleName || this.constructor.name;
127
+ StyleManager.applyStyles(this.shadowRoot, styleName, stylesToApply);
128
+ }
129
+ }
130
+
131
+ // 重新渲染JSX
155
132
  const content = this.render();
156
- this.shadowRoot.appendChild(content);
133
+
134
+ // 在添加到 DOM 之前恢复值,避免浏览器渲染状态值
135
+ // 这样可以确保值在元素添加到 DOM 之前就是正确的
136
+ if (focusState && focusState.key && focusState.value !== undefined) {
137
+ // 在 content 树中查找目标元素
138
+ const target = content.querySelector(
139
+ `[data-wsx-key="${focusState.key}"]`
140
+ ) as HTMLElement;
141
+
142
+ if (target) {
143
+ if (
144
+ target instanceof HTMLInputElement ||
145
+ target instanceof HTMLTextAreaElement
146
+ ) {
147
+ target.value = focusState.value;
148
+ }
149
+ }
150
+ }
151
+
152
+ // 恢复 adopted stylesheets (避免重新应用样式)
153
+ if (this.shadowRoot.adoptedStyleSheets) {
154
+ this.shadowRoot.adoptedStyleSheets = adoptedStyleSheets;
155
+ }
156
+
157
+ // 使用 requestAnimationFrame 批量执行 DOM 操作,减少重绘
158
+ // 在同一帧中完成添加和移除,避免中间状态
159
+ requestAnimationFrame(() => {
160
+ // 先添加新内容
161
+ this.shadowRoot.appendChild(content);
162
+
163
+ // 立即移除旧内容(在同一帧中,浏览器会批量处理)
164
+ const oldChildren = Array.from(this.shadowRoot.children).filter(
165
+ (child) => child !== content
166
+ );
167
+ oldChildren.forEach((child) => child.remove());
168
+
169
+ // 恢复焦点状态(在 DOM 替换之后)
170
+ // 值已经在添加到 DOM 之前恢复了,这里只需要恢复焦点和光标位置
171
+ // 使用另一个 requestAnimationFrame 确保 DOM 已完全更新
172
+ requestAnimationFrame(() => {
173
+ this.restoreFocusState(focusState);
174
+ // 清除待处理的焦点状态
175
+ this._pendingFocusState = null;
176
+ });
177
+ });
157
178
  } catch (error) {
158
- console.error(`[${this.constructor.name}] Error in rerender:`, error);
179
+ logger.error("Error in rerender:", error);
159
180
  this.renderError(error);
160
181
  }
161
182
  }
@@ -182,67 +203,6 @@ export abstract class WebComponent extends HTMLElement {
182
203
 
183
204
  this.shadowRoot.appendChild(errorElement);
184
205
  }
185
-
186
- /**
187
- * 获取配置值
188
- *
189
- * @param key - 配置键
190
- * @param defaultValue - 默认值
191
- * @returns 配置值
192
- */
193
- protected getConfig<T>(key: string, defaultValue?: T): T {
194
- return (this.config[key] as T) ?? (defaultValue as T);
195
- }
196
-
197
- /**
198
- * 设置配置值
199
- *
200
- * @param key - 配置键
201
- * @param value - 配置值
202
- */
203
- protected setConfig(key: string, value: unknown): void {
204
- this.config[key] = value;
205
- }
206
-
207
- /**
208
- * 获取属性值
209
- *
210
- * @param name - 属性名
211
- * @param defaultValue - 默认值
212
- * @returns 属性值
213
- */
214
- protected getAttr(name: string, defaultValue = ""): string {
215
- return this.getAttribute(name) || defaultValue;
216
- }
217
-
218
- /**
219
- * 设置属性值
220
- *
221
- * @param name - 属性名
222
- * @param value - 属性值
223
- */
224
- protected setAttr(name: string, value: string): void {
225
- this.setAttribute(name, value);
226
- }
227
-
228
- /**
229
- * 移除属性
230
- *
231
- * @param name - 属性名
232
- */
233
- protected removeAttr(name: string): void {
234
- this.removeAttribute(name);
235
- }
236
-
237
- /**
238
- * 检查是否有属性
239
- *
240
- * @param name - 属性名
241
- * @returns 是否存在
242
- */
243
- protected hasAttr(name: string): boolean {
244
- return this.hasAttribute(name);
245
- }
246
206
  }
247
207
 
248
208
  // 导出JSX助手
package/types/index.d.ts CHANGED
@@ -10,8 +10,8 @@ export type { JSXChildren } from "../src/jsx-factory";
10
10
  // 导出 WebComponent 类和配置
11
11
  export { WebComponent, type WebComponentConfig } from "../src/web-component";
12
12
 
13
- // 导出响应式 WebComponent 类和配置
14
- export { ReactiveWebComponent, type ReactiveWebComponentConfig } from "../src/reactive-component";
13
+ // 导出响应式装饰器 (@state)
14
+ export { state } from "../src/reactive-decorator";
15
15
 
16
16
  // 导出 LightComponent 类和配置
17
17
  export { LightComponent, type LightComponentConfig } from "../src/light-component";