@wsxjs/wsx-core 0.0.8 → 0.0.10

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
  }
@@ -11,24 +11,14 @@
11
11
  import { h, type JSXChildren } from "./jsx-factory";
12
12
  import { StyleManager } from "./styles/style-manager";
13
13
  import { BaseComponent, type BaseComponentConfig } from "./base-component";
14
+ import { createLogger } from "./utils/logger";
14
15
 
15
- /**
16
- * Web Component 配置接口
17
- */
18
- export interface WebComponentConfig extends BaseComponentConfig {
19
- preserveFocus?: boolean; // 是否在重渲染时保持焦点
20
- }
16
+ const logger = createLogger("WebComponent");
21
17
 
22
18
  /**
23
- * Type for focus data saved during rerender
19
+ * Web Component 配置接口
24
20
  */
25
- interface FocusData {
26
- tagName: string;
27
- className: string;
28
- value?: string;
29
- selectionStart?: number;
30
- selectionEnd?: number;
31
- }
21
+ export type WebComponentConfig = BaseComponentConfig;
32
22
 
33
23
  /**
34
24
  * 通用 WSX Web Component 基础抽象类
@@ -36,13 +26,10 @@ interface FocusData {
36
26
  export abstract class WebComponent extends BaseComponent {
37
27
  declare shadowRoot: ShadowRoot;
38
28
  protected config!: WebComponentConfig; // Initialized by BaseComponent constructor
39
- private _preserveFocus: boolean = true;
40
29
 
41
30
  constructor(config: WebComponentConfig = {}) {
42
31
  super(config);
43
32
  // BaseComponent already created this.config with getter for styles
44
- // Just update preserveFocus property
45
- this._preserveFocus = config.preserveFocus ?? true;
46
33
  this.attachShadow({ mode: "open" });
47
34
  // Styles are applied in connectedCallback for consistency with LightComponent
48
35
  // and to ensure all class properties (including getters) are initialized
@@ -62,9 +49,8 @@ export abstract class WebComponent extends BaseComponent {
62
49
  this.connected = true;
63
50
  try {
64
51
  // 应用CSS样式到Shadow DOM
65
- // Using method call ensures styles are available when accessed
66
- // This is consistent with LightComponent and avoids execution order issues
67
- const stylesToApply = this._getAutoStyles?.() || this.config.styles;
52
+ // Check both _autoStyles getter and config.styles getter
53
+ const stylesToApply = this._autoStyles || this.config.styles;
68
54
  if (stylesToApply) {
69
55
  const styleName = this.config.styleName || this.constructor.name;
70
56
  StyleManager.applyStyles(this.shadowRoot, styleName, stylesToApply);
@@ -74,10 +60,13 @@ export abstract class WebComponent extends BaseComponent {
74
60
  const content = this.render();
75
61
  this.shadowRoot.appendChild(content);
76
62
 
63
+ // 初始化事件监听器
64
+ this.initializeEventListeners();
65
+
77
66
  // 调用子类的初始化钩子
78
67
  this.onConnected?.();
79
68
  } catch (error) {
80
- console.error(`[${this.constructor.name}] Error in connectedCallback:`, error);
69
+ logger.error(`Error in connectedCallback:`, error);
81
70
  this.renderError(error);
82
71
  }
83
72
  }
@@ -87,6 +76,7 @@ export abstract class WebComponent extends BaseComponent {
87
76
  */
88
77
  disconnectedCallback(): void {
89
78
  this.connected = false;
79
+ this.cleanup(); // 清理资源(包括防抖定时器)
90
80
  this.onDisconnected?.();
91
81
  }
92
82
 
@@ -115,156 +105,79 @@ export abstract class WebComponent extends BaseComponent {
115
105
  */
116
106
  protected rerender(): void {
117
107
  if (!this.connected) {
118
- console.warn(
119
- `[${this.constructor.name}] Component is not connected, skipping rerender.`
120
- );
108
+ logger.warn("Component is not connected, skipping rerender.");
121
109
  return;
122
110
  }
123
111
 
124
- // 保存焦点状态(如果启用)
125
- let focusData: FocusData | null = null;
126
- if (this._preserveFocus && this.shadowRoot) {
127
- const activeElement = this.shadowRoot.activeElement;
128
- focusData = this.saveFocusState(activeElement);
129
- }
112
+ // 1. 捕获焦点状态(在 DOM 替换之前)
113
+ const focusState = this.captureFocusState();
114
+ // 保存到实例变量,供 render() 使用(如果需要)
115
+ this._pendingFocusState = focusState;
130
116
 
131
117
  // 保存当前的 adopted stylesheets (jsdom may not support this)
132
118
  const adoptedStyleSheets = this.shadowRoot.adoptedStyleSheets || [];
133
119
 
134
- // 清空现有内容但保留样式
135
- this.shadowRoot.innerHTML = "";
136
-
137
- // 恢复 adopted stylesheets (避免重新应用样式)
138
- if (this.shadowRoot.adoptedStyleSheets) {
139
- this.shadowRoot.adoptedStyleSheets = adoptedStyleSheets;
140
- }
141
-
142
- // 只有在没有 adopted stylesheets 时才重新应用样式
143
- // Check both _autoStyles getter and config.styles getter
144
- if (adoptedStyleSheets.length === 0) {
145
- const stylesToApply = this._autoStyles || this.config.styles;
146
- if (stylesToApply) {
147
- const styleName = this.config.styleName || this.constructor.name;
148
- StyleManager.applyStyles(this.shadowRoot, styleName, stylesToApply);
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
+ }
149
129
  }
150
- }
151
130
 
152
- // 重新渲染JSX
153
- try {
131
+ // 重新渲染JSX
154
132
  const content = this.render();
155
- this.shadowRoot.appendChild(content);
156
- } catch (error) {
157
- console.error(`[${this.constructor.name}] Error in rerender:`, error);
158
- this.renderError(error);
159
- }
160
-
161
- // 恢复焦点状态
162
- if (this._preserveFocus && focusData && this.shadowRoot) {
163
- this.restoreFocusState(focusData);
164
- }
165
- }
166
133
 
167
- /**
168
- * 保存焦点状态
169
- */
170
- private saveFocusState(activeElement: Element | null): FocusData | null {
171
- if (!activeElement) {
172
- return null;
173
- }
174
-
175
- const focusData: FocusData = {
176
- tagName: activeElement.tagName.toLowerCase(),
177
- className: activeElement.className,
178
- };
179
-
180
- // Save selection/cursor position
181
- if (activeElement.hasAttribute("contenteditable")) {
182
- const selection = window.getSelection();
183
- if (selection && selection.rangeCount > 0) {
184
- const range = selection.getRangeAt(0);
185
- focusData.selectionStart = range.startOffset;
186
- focusData.selectionEnd = range.endOffset;
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
+ }
187
150
  }
188
- }
189
151
 
190
- // Save input/select element state
191
- if (
192
- activeElement instanceof HTMLInputElement ||
193
- activeElement instanceof HTMLSelectElement
194
- ) {
195
- focusData.value = activeElement.value;
196
- if ("selectionStart" in activeElement) {
197
- focusData.selectionStart = activeElement.selectionStart ?? undefined;
198
- focusData.selectionEnd = activeElement.selectionEnd ?? undefined;
152
+ // 恢复 adopted stylesheets (避免重新应用样式)
153
+ if (this.shadowRoot.adoptedStyleSheets) {
154
+ this.shadowRoot.adoptedStyleSheets = adoptedStyleSheets;
199
155
  }
200
- }
201
156
 
202
- return focusData;
203
- }
157
+ // 使用 requestAnimationFrame 批量执行 DOM 操作,减少重绘
158
+ // 在同一帧中完成添加和移除,避免中间状态
159
+ requestAnimationFrame(() => {
160
+ // 先添加新内容
161
+ this.shadowRoot.appendChild(content);
204
162
 
205
- /**
206
- * 恢复焦点状态
207
- */
208
- private restoreFocusState(focusData: FocusData): void {
209
- if (!focusData) return;
210
-
211
- try {
212
- let targetElement: Element | null = null;
213
-
214
- // Try to find by className first (most specific)
215
- if (focusData.className) {
216
- targetElement = this.shadowRoot.querySelector(
217
- `.${focusData.className.split(" ")[0]}`
163
+ // 立即移除旧内容(在同一帧中,浏览器会批量处理)
164
+ const oldChildren = Array.from(this.shadowRoot.children).filter(
165
+ (child) => child !== content
218
166
  );
219
- }
220
-
221
- // Fallback: find by tag name
222
- if (!targetElement) {
223
- targetElement = this.shadowRoot.querySelector(focusData.tagName);
224
- }
225
-
226
- if (targetElement) {
227
- // Restore focus - prevent page scroll
228
- (targetElement as HTMLElement).focus({ preventScroll: true });
229
-
230
- // Restore selection/cursor position
231
- if (focusData.selectionStart !== undefined) {
232
- if (targetElement instanceof HTMLInputElement) {
233
- targetElement.setSelectionRange(
234
- focusData.selectionStart,
235
- focusData.selectionEnd ?? focusData.selectionStart
236
- );
237
- } else if (targetElement instanceof HTMLSelectElement) {
238
- targetElement.value = focusData.value ?? "";
239
- } else if (targetElement.hasAttribute("contenteditable")) {
240
- this.setCursorPosition(targetElement, focusData.selectionStart);
241
- }
242
- }
243
- }
244
- } catch {
245
- // Silently handle focus restoration failure
246
- }
247
- }
248
-
249
- /**
250
- * 设置光标位置
251
- */
252
- private setCursorPosition(element: Element, position: number): void {
253
- try {
254
- const selection = window.getSelection();
255
- if (selection) {
256
- const range = document.createRange();
257
- const textNode = element.childNodes[0];
258
- if (textNode) {
259
- const maxPos = Math.min(position, textNode.textContent?.length || 0);
260
- range.setStart(textNode, maxPos);
261
- range.setEnd(textNode, maxPos);
262
- selection.removeAllRanges();
263
- selection.addRange(range);
264
- }
265
- }
266
- } catch {
267
- // Silently handle cursor position failure
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
+ });
178
+ } catch (error) {
179
+ logger.error("Error in rerender:", error);
180
+ this.renderError(error);
268
181
  }
269
182
  }
270
183