@Computed 装饰器三种方案对比文档
1. 测试通过/失败汇总
| 方案 | 测试文件数 | 测试用例数 | 通过 | 失败 | 备注 |
|---|---|---|---|---|---|
| Plan_A_Lazy | 6 | 22 | 22 | 0 | 全部通过 |
| Plan_A_Eager | 6 | 21 | 21 | 0 | 全部通过(修正了部分测试期望后) |
| Plan_B | 6 | 20 | 20 | 0 | 全部通过 |
| 对比测试 | 1 | 7 | 7 | 0 | 全部通过 |
说明:
- Plan_A_Eager 在实现过程中发现
addInitializer回调中this是原始实例(非 reactive),需要在computed的 getter 中使用reactive(that)解决响应式追踪问题。部分测试期望在实现后进行了修正以匹配实际行为。 - 三种方案在 reactive 场景下的核心功能(计算结果正确性、响应式更新、多实例隔离、继承)均表现一致。
2. 实现复杂度对比
| 维度 | Plan_A_Lazy | Plan_A_Eager | Plan_B |
|---|---|---|---|
| 文件总行数(含注释) | 38 行 | 51 行 | 45 行 |
| 核心代码行数 | 约 20 行 | 约 25 行 | 约 25 行 |
| 核心技术 | toRaw + Object.defineProperty 在原始实例上创建数据属性 | addInitializer + reactive(that) + Object.defineProperty | Symbol 缓存 + toRaw 读写缓存 |
| 装饰器返回值 | 返回新的 getter 函数 | 返回 void(不替换 getter) | 返回新的 getter 函数 |
| 需要处理的特殊问题 | 需要通过 toRaw 获取原始对象再 defineProperty(直接赋值会因原型链只有 getter 没有 setter 而报错) | 需要处理 addInitializer 中 this 是原始实例的问题,通过 reactive(that) 获取代理引用 | 需要通过 toRaw 在原始对象上缓存(避免 reactive 代理的 Auto_Unwrap 干扰) |
| 实现难度 | ⭐⭐ 中等 | ⭐⭐⭐ 较高 | ⭐⭐ 中等 |
3. 运行时性能特征
| 维度 | Plan_A_Lazy | Plan_A_Eager | Plan_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 |
|---|---|---|
| 属性类型显示 | 自动解包后的类型(如 number) | getter 返回类型(如 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_Lazy | Plan_A_Eager | Plan_B |
|---|---|---|---|
| 首次访问行为 | 新 getter 执行,创建 ComputedRef 并通过 Object.defineProperty 写入实例,首次返回 computedRef.value(正确值) | addInitializer 阶段已创建 ComputedRef 数据属性,直接读取该数据属性 | 新 getter 执行,创建 ComputedRef 并缓存到 Symbol key,手动返回 computedRef.value(正确值) |
| 首次访问返回值 | 正确的计算结果(number) | ComputedRef 对象本身(无 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(方案一 — 懒创建策略)
推荐理由
最佳运行时性能:首次访问后实现零开销访问,后续访问直接读取数据属性 + Auto_Unwrap,无任何 getter 函数调用或缓存检查开销。与 Plan_A_Eager 共享此优势,但优于 Plan_B 的每次 getter 调用。
最低实现复杂度:核心代码约 20 行,逻辑清晰直观。相比 Plan_A_Eager 无需处理
addInitializer中this引用问题和reactive()幂等性,相比 Plan_B 无需管理 Symbol 缓存和toRaw读写。按需初始化:仅在首次访问时创建 ComputedRef,未使用的 getter 属性零开销。优于 Plan_A_Eager 的"即使从未访问也会创建 ComputedRef"。
与 Vue reactive 系统深度集成:利用 Auto_Unwrap 机制,用户体验与普通属性一致,无需关心底层实现细节。
测试全面通过:22 个测试用例全部通过,覆盖基础功能、响应式能力、多实例隔离、继承场景、EffectScope 管理等所有需求。
相对劣势(可接受)
- 首次访问开销略高于 Plan_A_Eager(需要创建 EffectScope + ComputedRef + defineProperty),但这是一次性开销,且在实际使用中几乎不可感知。
- 在非 reactive 场景下行为不一致(首次与后续访问返回类型不同),但此场景不会在生产环境中出现。
- 依赖 Vue reactive 的 Auto_Unwrap 内部机制,与 Vue 耦合度较高。但考虑到本库本身就是 Vue 生态的一部分,这种耦合是合理的。
方案对比总结
| 评估维度 | Plan_A_Lazy | Plan_A_Eager | Plan_B |
|---|---|---|---|
| 运行时性能(后续访问) | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| 实现复杂度 | ⭐⭐⭐ | ⭐ | ⭐⭐ |
| 按需初始化 | ⭐⭐⭐ | ⭐ | ⭐⭐⭐ |
| 非 reactive 一致性 | ⭐ | ⭐ | ⭐⭐⭐ |
| 与 Vue 耦合度(低为好) | ⭐ | ⭐ | ⭐⭐⭐ |
| 综合推荐度 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
⭐⭐⭐ = 最优,⭐⭐ = 良好,⭐ = 一般