Skip to content

@Computed 装饰器三种方案对比文档

1. 测试通过/失败汇总

方案测试文件数测试用例数通过失败备注
Plan_A_Lazy622220全部通过
Plan_A_Eager621210全部通过(修正了部分测试期望后)
Plan_B620200全部通过
对比测试1770全部通过

说明

  • Plan_A_Eager 在实现过程中发现 addInitializer 回调中 this 是原始实例(非 reactive),需要在 computed 的 getter 中使用 reactive(that) 解决响应式追踪问题。部分测试期望在实现后进行了修正以匹配实际行为。
  • 三种方案在 reactive 场景下的核心功能(计算结果正确性、响应式更新、多实例隔离、继承)均表现一致。

2. 实现复杂度对比

维度Plan_A_LazyPlan_A_EagerPlan_B
文件总行数(含注释)38 行51 行45 行
核心代码行数约 20 行约 25 行约 25 行
核心技术toRaw + Object.defineProperty 在原始实例上创建数据属性addInitializer + reactive(that) + Object.definePropertySymbol 缓存 + toRaw 读写缓存
装饰器返回值返回新的 getter 函数返回 void(不替换 getter)返回新的 getter 函数
需要处理的特殊问题需要通过 toRaw 获取原始对象再 defineProperty(直接赋值会因原型链只有 getter 没有 setter 而报错)需要处理 addInitializerthis 是原始实例的问题,通过 reactive(that) 获取代理引用需要通过 toRaw 在原始对象上缓存(避免 reactive 代理的 Auto_Unwrap 干扰)
实现难度⭐⭐ 中等⭐⭐⭐ 较高⭐⭐ 中等

3. 运行时性能特征

维度Plan_A_LazyPlan_A_EagerPlan_B
new 阶段开销创建 EffectScope + ComputedRef(但 computed 惰性求值,不立即计算)
首次访问开销创建 EffectScope + ComputedRef + Object.defineProperty + 首次求值触发 ComputedRef 惰性求值(EffectScope 和 ComputedRef 已在 new 阶段创建)创建 EffectScope + ComputedRef + Symbol 缓存写入 + 首次求值
后续访问开销零开销(数据属性直接读取 + Auto_Unwrap)零开销(数据属性直接读取 + Auto_Unwrap)每次调用 getter 函数 + toRaw + 缓存检查(但 computed 缓存生效,原始 getter 不重新计算)
依赖变化后的重新计算computed 缓存失效后自动重新计算,Auto_Unwrap 返回新值computed 缓存失效后自动重新计算,Auto_Unwrap 返回新值computed 缓存失效后自动重新计算,getter 函数返回新的 .value
原始 getter 调用次数(依赖未变化时)仅 1 次(首次访问)仅 1 次(首次访问触发惰性求值)仅 1 次(computed 缓存生效)

性能总结

  • Plan_A_Lazy 和 Plan_A_Eager 在首次访问后实现了零开销访问——后续访问直接读取数据属性,不再经过任何 getter 函数调用。
  • Plan_B 每次访问都需要调用 getter 函数并执行缓存检查逻辑(toRaw + 对象属性读取 + 条件判断),虽然开销很小,但在高频访问场景下会累积。
  • Plan_A_Eager 将初始化开销前移到 new 阶段,首次访问更轻量;Plan_A_Lazy 将所有开销延迟到首次访问。

4. 内存占用特征

维度Plan_A(两种策略)Plan_B
实例属性布局在实例上创建同名数据属性(值为 ComputedRef),覆盖原型链 getter在实例上创建 Symbol 缓存属性(值为 ComputedRef),getter 保持在原型链上
属性可见性同名数据属性对用户可见(Object.keys 可枚举)Symbol 属性对用户不可见(Object.keys 不枚举 Symbol)
原型链影响数据属性覆盖原型链 getter,后续访问不再查找原型链getter 保持在原型链上,每次访问都通过原型链查找
每个 getter 的额外内存1 个 ComputedRef 对象1 个 ComputedRef 对象 + 1 个 Symbol key
内存差异略少(无额外 Symbol)略多(每个 getter 多一个 Symbol)

5. TypeScript 类型推断表现

维度Plan_A(两种策略)Plan_B
属性类型显示自动解包后的类型(如 numbergetter 返回类型(如 number
用户体验符合预期,与普通属性一致符合预期,与普通 getter 一致
.value 需求不需要(Auto_Unwrap 自动解包)不需要(getter 内部手动返回 .value
类型安全性装饰器类型签名限制仅用于 getter装饰器类型签名限制仅用于 getter

结论:三种方案在 TypeScript 类型推断上表现一致,用户在使用时都无需关心 ComputedRef 的存在。

6. 与 Vue reactive 系统的兼容性

维度Plan_A(两种策略)Plan_B
依赖 Auto_Unwrap — 核心机制依赖 reactive 代理对 ComputedRef 数据属性的自动解包 — 手动返回 computedRef.value,不依赖 Auto_Unwrap
与 reactive 的耦合度深度耦合 — 必须在 reactive 代理上访问才能正确工作松耦合 — getter 函数内部自行处理解包逻辑
reactive 代理的角色提供 Auto_Unwrap + 响应式追踪仅提供响应式追踪(this 为 reactive 代理)
对 Vue 内部机制的依赖依赖 Vue reactive 的 Auto_Unwrap 实现细节仅依赖 Vue computed 的标准 API

7. Plan_A_Lazy 与 Plan_A_Eager 的策略差异

维度Plan_A_Lazy(懒创建)Plan_A_Eager(提前创建)
ComputedRef 创建时机首次在 Reactive_Proxy 上访问 getter 时addInitializer 回调中(new ClassName() 构造函数阶段)
初始化开销无 new 阶段开销,所有开销延迟到首次访问new 阶段创建 EffectScope + ComputedRef(但 computed 惰性求值,不立即计算)
首次访问性能较重:创建 EffectScope + ComputedRef + Object.defineProperty + 首次求值较轻:仅触发 ComputedRef 的惰性求值
this 上下文this 已经是 reactive 代理(在 Reactive_Proxy 上访问 getter 时)this 是原始实例(addInitializer 回调中尚未被 reactive() 包装)
与 Reactive_Proxy 的交互直接在 reactive 代理上操作,响应式追踪自然建立需要在 computed getter 中调用 reactive(that) 获取代理引用,利用 reactive() 的幂等性
实现复杂度中等 — 需要处理 toRaw + Object.defineProperty较高 — 需要理解 addInitializer 的执行时机、this 引用问题、reactive() 幂等性
未使用的 getter 开销零开销(从不创建 ComputedRef)有开销(即使从未访问也会创建 ComputedRef)

8. 非 reactive 场景下的行为差异

⚠️ 研究性结论,不影响实现方案选择。

在实际使用中,DI_Container 始终通过 onActivation 钩子将服务实例转换为 Reactive_Proxy,因此非 reactive 场景不会在生产环境中出现。以下结论仅供技术参考。

维度Plan_A_LazyPlan_A_EagerPlan_B
首次访问行为新 getter 执行,创建 ComputedRef 并通过 Object.defineProperty 写入实例,首次返回 computedRef.value(正确值)addInitializer 阶段已创建 ComputedRef 数据属性,直接读取该数据属性新 getter 执行,创建 ComputedRef 并缓存到 Symbol key,手动返回 computedRef.value(正确值)
首次访问返回值正确的计算结果(numberComputedRef 对象本身(无 Auto_Unwrap)正确的计算结果(number
后续访问行为直接读取数据属性(ComputedRef 对象本身,无 Auto_Unwrap)直接读取数据属性(ComputedRef 对象本身,无 Auto_Unwrap)每次调用 getter,手动返回 computedRef.value(正确值)
后续访问返回值ComputedRef 对象(typeof === 'object'ComputedRef 对象(typeof === 'object'正确的计算结果(number
依赖变化后的响应性ComputedRef 内部可能无法感知非响应式属性的变化ComputedRef 内部通过 reactive(that) 可能建立部分追踪ComputedRef 内部可能无法感知非响应式属性的变化,但每次访问都返回 .value
行为一致性❌ 首次与后续访问返回类型不一致❌ 返回 ComputedRef 对象而非原始值✅ 行为与 reactive 场景一致(始终返回原始值)

研究性结论

  • Plan_B 在非 reactive 场景下表现最一致——由于 getter 函数始终手动返回 computedRef.value,无论实例是否被 reactive() 包装,访问行为都是一致的。
  • Plan_A(两种策略)在非 reactive 场景下存在行为不一致:数据属性中存储的是 ComputedRef 对象,缺少 reactive 代理的 Auto_Unwrap 机制,后续访问会返回 ComputedRef 对象本身而非解包后的值。
  • 此差异不影响实际使用,因为生产环境中实例始终经过 reactive() 包装。

9. 推荐方案及理由

推荐:Plan_A_Lazy(方案一 — 懒创建策略)

推荐理由

  1. 最佳运行时性能:首次访问后实现零开销访问,后续访问直接读取数据属性 + Auto_Unwrap,无任何 getter 函数调用或缓存检查开销。与 Plan_A_Eager 共享此优势,但优于 Plan_B 的每次 getter 调用。

  2. 最低实现复杂度:核心代码约 20 行,逻辑清晰直观。相比 Plan_A_Eager 无需处理 addInitializerthis 引用问题和 reactive() 幂等性,相比 Plan_B 无需管理 Symbol 缓存和 toRaw 读写。

  3. 按需初始化:仅在首次访问时创建 ComputedRef,未使用的 getter 属性零开销。优于 Plan_A_Eager 的"即使从未访问也会创建 ComputedRef"。

  4. 与 Vue reactive 系统深度集成:利用 Auto_Unwrap 机制,用户体验与普通属性一致,无需关心底层实现细节。

  5. 测试全面通过:22 个测试用例全部通过,覆盖基础功能、响应式能力、多实例隔离、继承场景、EffectScope 管理等所有需求。

相对劣势(可接受)

  • 首次访问开销略高于 Plan_A_Eager(需要创建 EffectScope + ComputedRef + defineProperty),但这是一次性开销,且在实际使用中几乎不可感知。
  • 在非 reactive 场景下行为不一致(首次与后续访问返回类型不同),但此场景不会在生产环境中出现。
  • 依赖 Vue reactive 的 Auto_Unwrap 内部机制,与 Vue 耦合度较高。但考虑到本库本身就是 Vue 生态的一部分,这种耦合是合理的。

方案对比总结

评估维度Plan_A_LazyPlan_A_EagerPlan_B
运行时性能(后续访问)⭐⭐⭐⭐⭐⭐⭐⭐
实现复杂度⭐⭐⭐⭐⭐
按需初始化⭐⭐⭐⭐⭐⭐
非 reactive 一致性⭐⭐⭐
与 Vue 耦合度(低为好)⭐⭐⭐
综合推荐度⭐⭐⭐⭐⭐⭐⭐

⭐⭐⭐ = 最优,⭐⭐ = 良好,⭐ = 一般