/** * Base Component for WSX Web Components * * Provides common functionality shared by WebComponent and LightComponent: * - Reactive state management (reactive, useState, scheduleRerender) * - Configuration management (getConfig, setConfig) * - Attribute helpers (getAttr, setAttr, removeAttr, hasAttr) * - Lifecycle hooks (onConnected, onDisconnected, onAttributeChanged) */ import { reactive as createReactive, createState, reactiveWithDebug } from "./utils/reactive"; import { DOMCacheManager } from "./dom-cache-manager"; /** * Type for reactive state storage */ interface ReactiveStateStorage { getter: () => unknown; setter: (value: unknown | ((prev: unknown) => unknown)) => void; } /** * Base configuration interface */ export interface BaseComponentConfig { styles?: string; autoStyles?: string; styleName?: string; debug?: boolean; [key: string]: unknown; } /** * Base Component class with common functionality * * This class provides shared functionality for both WebComponent and LightComponent. * It should not be used directly - use WebComponent or LightComponent instead. */ export abstract class BaseComponent extends HTMLElement { protected config: BaseComponentConfig; protected connected: boolean = false; protected _isDebugEnabled: boolean = false; protected _reactiveStates = new Map(); /** * Auto-injected styles from Babel plugin (if CSS file exists) * @internal - Managed by babel-plugin-wsx-style */ protected _autoStyles?: string; /** * Unique instance ID for this component * Used for generating stable cache keys (RFC 0059) * Can be manually set or auto-generated by cache-key.ts if missing * @internal */ public _wsxInstanceId?: string; /** * DOM Cache Manager for fine-grained updates (RFC 0037) * @internal */ protected _domCache = new DOMCacheManager(); /** * 正在渲染标志(防止在 _rerender() 执行期间再次触发 scheduleRerender()) * @internal */ protected _isRendering: boolean = false; /** * 已调度渲染标志(防止在同一事件循环中重复注册 requestAnimationFrame) * 用于批量更新:同一事件循环中的多个状态变化只触发一次渲染 * @internal */ private _hasScheduledRender: boolean = false; /** * 子类应该重写这个方法来定义观察的属性 * @returns 要观察的属性名数组 */ static get observedAttributes(): string[] { return []; } constructor(config: BaseComponentConfig = {}) { super(); this._isDebugEnabled = config.debug ?? false; // eslint-disable-next-line @typescript-eslint/no-this-alias const host = this; // Store original styles value to avoid infinite recursion in getter const originalStyles = config.styles; this.config = { ...config, get styles() { // Auto-detect injected styles from class property // Note: _defineProperty executes in constructor after super(), // so we check _autoStyles dynamically via getter // This works for both WebComponent and LightComponent // Priority: originalStyles > _autoStyles const result = originalStyles || host._autoStyles || ""; return result; }, set styles(value: string) { config.styles = value; }, }; } /** * 抽象方法:子类必须实现JSX渲染 * * @returns JSX元素 */ abstract render(): HTMLElement | SVGElement; /** * 可选生命周期钩子:组件已连接 */ protected onConnected?(): void; /** * 可选生命周期钩子:组件渲染完成后调用 * 在 DOM 更新完成后调用,适合执行需要访问 DOM 的操作(如语法高亮、初始化第三方库等) */ protected onRendered?(): void; /** * Gets the DOMCacheManager instance. * @internal */ public getDomCache(): DOMCacheManager { return this._domCache; } /** * 可选生命周期钩子:组件已断开 */ protected onDisconnected?(): void; /** * 可选生命周期钩子:属性已更改 */ protected onAttributeChanged?(name: string, oldValue: string, newValue: string): void; /** * Web Component生命周期:属性变化 */ attributeChangedCallback(name: string, oldValue: string, newValue: string): void { this.onAttributeChanged?.(name, oldValue, newValue); } /** * 创建响应式对象 * * @param obj 要变为响应式的对象 * @param debugName 调试名称(可选) * @returns 响应式代理对象 */ protected reactive(obj: T, debugName?: string): T { const reactiveFn = this._isDebugEnabled ? reactiveWithDebug : createReactive; const name = debugName || `${this.constructor.name}.reactive`; return this._isDebugEnabled ? reactiveFn(obj, () => this.scheduleRerender(), name) : reactiveFn(obj, () => this.scheduleRerender()); } /** * 创建响应式状态 * * @param key 状态标识符 * @param initialValue 初始值 * @returns [getter, setter] 元组 */ protected useState( key: string, initialValue: T ): [() => T, (value: T | ((prev: T) => T)) => void] { if (!this._reactiveStates.has(key)) { const [getter, setter] = createState(initialValue, () => this.scheduleRerender()); this._reactiveStates.set(key, { getter: getter as () => unknown, setter: setter as (value: unknown | ((prev: unknown) => unknown)) => void, }); } const state = this._reactiveStates.get(key); if (!state) { throw new Error(`State ${key} not found`); } return [state.getter as () => T, state.setter as (value: T | ((prev: T) => T)) => void]; } /** * 调度重渲染 * 这个方法被响应式系统调用,开发者通常不需要直接调用 * 使用 queueMicrotask 进行异步调度,与 reactive() 系统保持一致 */ protected scheduleRerender(): void { if (!this.connected) { return; } // 如果正在渲染,跳过本次调度(防止无限循环) if (this._isRendering) { return; } // 如果已经调度了渲染,跳过(避免在同一事件循环中重复注册 requestAnimationFrame) // 这实现了批量更新:同一事件循环中的多个状态变化只触发一次渲染 if (this._hasScheduledRender) { return; } // 标记已调度渲染(批量更新的关键) this._hasScheduledRender = true; // 使用 requestAnimationFrame 而不是 queueMicrotask,确保在渲染帧中执行 // 这样可以避免在 render() 执行期间触发的 scheduleRerender() 立即执行 requestAnimationFrame(() => { // 重置调度标志(允许后续的状态变化调度新的渲染) this._hasScheduledRender = false; if (this.connected && !this._isRendering) { // 设置渲染标志,防止在 _rerender() 执行期间再次触发 // 注意:_isRendering 标志会在 _rerender() 的 onRendered() 调用完成后清除 this._isRendering = true; // 调用 _rerender() 执行实际渲染(不再调用 rerender(),避免循环) // _isRendering 标志会在 _rerender() 完成所有异步操作后清除 this._rerender(); } else if (!this.connected) { // 如果组件已断开,确保清除渲染标志 this._isRendering = false; } }); } /** * 调度重渲染(公开 API) * * 与 scheduleRerender() 对齐:所有重渲染都通过统一的调度机制 * 使用异步调度机制,自动处理防抖和批量更新 * * 注意:此方法现在是异步的,使用调度机制 * 如果需要同步执行,使用 _rerender()(不推荐,仅内部使用) */ protected rerender(): void { // 对齐到 scheduleRerender(),统一调度机制 this.scheduleRerender(); } /** * 内部重渲染实现(同步执行) * 由 scheduleRerender() 在适当时机调用 * * @internal - 子类需要实现此方法 */ protected abstract _rerender(): void; /** * 清理资源(在组件断开连接时调用) * @internal */ protected cleanup(): void { // 清理逻辑已简化,不再需要清除定时器或事件监听器 } /** * 初始化事件监听器(在组件连接时调用) * @internal */ protected initializeEventListeners(): void { // 初始化逻辑已简化,不再需要添加 blur 监听器 } /** * 获取配置值 * * @param key - 配置键 * @param defaultValue - 默认值 * @returns 配置值 */ protected getConfig(key: string, defaultValue?: T): T { return (this.config[key] as T) ?? (defaultValue as T); } /** * 设置配置值 * * @param key - 配置键 * @param value - 配置值 */ protected setConfig(key: string, value: unknown): void { this.config[key] = value; } /** * 获取属性值 * * @param name - 属性名 * @param defaultValue - 默认值 * @returns 属性值 */ protected getAttr(name: string, defaultValue = ""): string { return this.getAttribute(name) || defaultValue; } /** * 设置属性值 * * @param name - 属性名 * @param value - 属性值 */ protected setAttr(name: string, value: string): void { this.setAttribute(name, value); } /** * 移除属性 * * @param name - 属性名 */ protected removeAttr(name: string): void { this.removeAttribute(name); } /** * 检查是否有属性 * * @param name - 属性名 * @returns 是否存在 */ protected hasAttr(name: string): boolean { return this.hasAttribute(name); } /** * 清理响应式状态 */ protected cleanupReactiveStates(): void { this._reactiveStates.clear(); } /** * 获取当前活动的 DOM 根(Shadow DOM 或 Light DOM) * @returns 活动的 DOM 根元素 */ protected getActiveRoot(): ShadowRoot | HTMLElement { // WebComponent 使用 shadowRoot,LightComponent 使用自身 if ("shadowRoot" in this && this.shadowRoot) { return this.shadowRoot; } return this; } }