UNPKG

@vitarx/responsive

Version:
322 lines (321 loc) 11.2 kB
var _a, _b, _c; import { isFunction, microTaskDebouncedCallback } from '@vitarx/utils'; import { Depend } from '../../../depend/index.js'; import { EffectScope } from '../../../effect/index.js'; import { Observer, Subscriber } from '../../../observer/index.js'; import { DEEP_SIGNAL_SYMBOL, REF_SIGNAL_SYMBOL, SIGNAL_RAW_VALUE_SYMBOL, SIGNAL_SYMBOL, SignalManager } from '../../core/index.js'; /** * # 计算属性 * * 计算属性是一种特殊的响应式数据,它的值由一个getter函数计算得出。 * 当依赖的响应式数据发生变化时,计算属性会自动重新计算并更新其值。 * * @template T - 计算结果的类型 * @implements {RefSignal<T>} - 实现RefSignal接口,使其可以像普通的响应式引用一样使用 * * @example * ```ts * const count = ref(0) * const double = new Computed(() => count.value * 2) * console.log(double.value) // 0 * count.value = 2 * console.log(double.value) // 4 * ``` */ export class Computed { /** * 构造一个计算属性对象 * * @param {(oldValue: T | undefined) => T} getter - 计算属性的getter函数,接收上一次的计算结果作为参数 * @param {ComputedOptions<T>} [options={}] - 计算属性的配置选项 * @param {(newValue: T) => void} [options.setter] - 计算属性的setter函数,用于处理对计算属性的赋值操作 * @param {boolean} [options.immediate=false] - 是否立即计算,默认为false,首次访问时才计算 * @param {boolean} [options.scope=true] - 是否添加到当前作用域,默认为true,作用域销毁时自动清理 * @param {boolean} [options.batch=true] - 是否使用批处理模式,默认为true,多个连续的变更会合并为一次计算 */ constructor(getter, options = {}) { /** * 标识是否为深度响应式对象 * 计算属性不支持深度响应,始终为false */ Object.defineProperty(this, _a, { enumerable: true, configurable: true, writable: true, value: false }); /** * 标识为引用类型的响应式信号 */ Object.defineProperty(this, _b, { enumerable: true, configurable: true, writable: true, value: true }); /** * 标识为响应式信号对象 */ Object.defineProperty(this, _c, { enumerable: true, configurable: true, writable: true, value: true }); /** * 计算结果缓存 * @private */ Object.defineProperty(this, "_computedResult", { enumerable: true, configurable: true, writable: true, value: undefined }); /** * 计算属性的getter函数 * @private */ Object.defineProperty(this, "_getter", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * 计算属性的配置选项 * @private */ Object.defineProperty(this, "_options", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * 依赖变化的订阅处理器 * @private */ Object.defineProperty(this, "_handler", { enumerable: true, configurable: true, writable: true, value: undefined }); /** * 作用域 * @private */ Object.defineProperty(this, "_scope", { enumerable: true, configurable: true, writable: true, value: undefined }); /** * 是否已初始化 * @private */ Object.defineProperty(this, "_initialize", { enumerable: true, configurable: true, writable: true, value: false }); this._getter = getter; this._options = Object.assign({ immediate: false, scope: true, batch: true }, options); if (this._options.scope) this._scope = EffectScope.getCurrentScope(); // 如果设置了立即计算,则在构造时就初始化 if (options.immediate) this.init(); } /** * 获取初始化状态的访问器 * 返回一个布尔值,表示对象是否已初始化 * @returns {boolean} 返回内部的_initialize属性值 */ get initialize() { return this._initialize; } /** * 获取计算属性的原始值 * 实现BaseSignal接口的SIGNAL_RAW_VALUE_SYMBOL属性 * * @returns {T} 计算结果的原始值 */ get [(_a = DEEP_SIGNAL_SYMBOL, _b = REF_SIGNAL_SYMBOL, _c = SIGNAL_SYMBOL, SIGNAL_RAW_VALUE_SYMBOL)]() { return this.value; } /** * 获取计算结果 * * 如果计算属性尚未初始化,则会先进行初始化。 * 每次访问都会追踪依赖,以便在依赖变化时能够正确地通知订阅者。 * * @returns {T} 计算结果 */ get value() { // 如果尚未初始化,则先初始化 this.init(); // 追踪对value属性的访问 Depend.track(this, 'value'); return this._computedResult; } /** * 修改计算结果 * * 如果提供了setter函数,则调用setter函数处理新值; * 否则,输出警告信息,提示计算属性不应该被直接修改。 * * @param {T} newValue - 要设置的新值 */ set value(newValue) { if (typeof this._options.setter === 'function') { this._options.setter(newValue); } else if (import.meta.env.DEV) { console.warn('[Computed]:Computed properties should not be modified directly unless a setter function is defined。'); } } /** * 将计算属性转换为字符串 * * 如果计算结果有toString方法,则调用该方法; * 否则,返回格式化的类型描述。 * * @returns {string} 字符串表示 */ toString() { if (this.value?.toString && isFunction(this.value.toString)) { return this.value.toString(); } else { return `[Object Computed<${typeof this.value}>]`; } } /** * 停止监听依赖变化 * * 调用此方法会停止对依赖的监听,并释放相关监听器。 * 计算属性将不再响应依赖的变化,但仍然保留最后一次计算的结果。 * * @returns {T} 最后一次的计算结果 */ stop() { if (this._handler) { this._handler.dispose(); this._handler = undefined; this._scope = undefined; } else { this._computedResult = this._getter(undefined); } return this._computedResult; } /** * 定义当对象需要转换成原始值时的行为 * * 根据不同的转换提示返回适当的值: * - 'number': 返回计算结果,尝试进行数值转换 * - 'string': 调用toString方法获取字符串表示 * - 'default': 返回计算结果 * * @param {string} hint - 转换提示类型 * @returns {any} 根据提示类型转换后的原始值 */ [Symbol.toPrimitive](hint) { switch (hint) { case 'number': return this.value; case 'string': return this.toString(); case 'default': return this.value; } } /** * 设置作用域 * * 此方法仅在计算属性被初始化之前设置有效 * * @param {boolean | EffectScope} scope - 作用域或boolean值,表示是否允许添加到作用域 * @returns {this} 当前实例,支持链式调用 */ scope(scope) { if (this.initialize) return this; if (scope instanceof EffectScope) { this._scope = scope; } else if (scope) { this._scope = EffectScope.getCurrentScope(); } return this; } /** * 手动初始化计算属性 * * 执行以下步骤: * 1. 收集getter函数执行过程中的依赖 * 2. 缓存计算结果 * 3. 如果有依赖,创建订阅处理器监听依赖变化 * 4. 设置清理函数,在销毁时移除订阅 * * @returns {this} 当前实例,支持链式调用 */ init() { if (this.initialize) return this; // 标记为已初始化 this._initialize = true; // 收集依赖并获取初始计算结果 const { result, deps } = Depend.collect(() => this._getter(this._computedResult), 'exclusive'); // 缓存计算结果 this._computedResult = result; // 如果有依赖,则创建订阅处理器 if (deps.size > 0) { const handler = () => { // 重新计算结果 const newResult = this._getter(this._computedResult); // 只有当结果发生变化时才通知订阅者 if (newResult !== this._computedResult) { this._computedResult = newResult; SignalManager.notifySubscribers(this, 'value'); } }; // 创建订阅处理器,使用微任务延迟执行以提高性能 this._handler = new Subscriber(!this._options.batch ? handler : microTaskDebouncedCallback(handler), { scope: false }); // 如果存在作用域,则添加到作用域中 if (this._scope) this._scope.addEffect(this._handler); // 为每个依赖添加订阅 deps.forEach((props, signal) => { for (const prop of props) { Observer.addSubscriber(signal, prop, this._handler, { batch: false, // 禁用批处理,确保及时更新 autoRemove: false // 不自动移除,由onDispose处理 }); } }); // 设置清理函数,在销毁时移除所有订阅 this._handler.onDispose(() => { deps.forEach((props, signal) => { for (const prop of props) { Observer.removeSubscriber(signal, prop, this._handler, false); } }); this._handler = undefined; this._scope = undefined; }); } else if (import.meta.env.DEV) { console.warn('[Computed]:No dependencies detected in computed property. The computed value will not automatically update when data changes. Consider checking if your getter function accesses signal properties correctly.'); } return this; } }