Vue 2.6 到 2.7 升级中嵌套 Ref 解包问题

Aditya2025-04-23前端VueRef

问题背景

在 Vue 2.6 中使用 @vue/composition-api@1.1.13 时,开发者可通过 Composition API 管理响应式逻辑。然而,升级到 Vue 2.7 后,由于框架内置了 Composition API 并引入了 嵌套 Ref 自动解包 特性(类似 Vue 3),原有代码可能因响应式结构变化而失效。以下是一个典型问题场景:

原始代码(Vue 2.6 正常,Vue 2.7 异常)

export abstract class Shape {
  status?: Ref<STATUS> = ref(STATUS.NONE); // Status 定义为 Ref 类型

  constructor(options) {
    // 监听 status 的变化(Vue 2.7 中失效)
    watch(this.status, (newVal) => {
      console.log("Status changed:", newVal);
    });
  }
}

// 将实例存入 Ref 数组
const areaLines = ref<HotareaVisible<Line>[]>([]);
areaLines.value.push(new Line({ ... }));

// 监听数组变化(可能因嵌套 Ref 解包导致异常)
watch(areaLines, (lines) => { ... });
  • Vue 2.6 行为status 作为 Ref 对象被正确监听,areaLines 中的 Shape 实例保持原始结构。
  • Vue 2.7 问题: 自动解包导致 status 被转换为原始值,watch(this.status, ...) 失效,且 areaLines 的响应式逻辑异常。

问题根源

1. Vue 2.7 的自动解包机制

Vue 2.7 在访问嵌套 Ref 时,会 自动提取最内层值,而非保留 Ref 对象。例如:

class Shape {
  status = ref(0);
}
const container = ref([new Shape()]);

// Vue 2.7 输出:status 被解包为 0
console.log(container.value[0].status); // 0(而非 Ref 对象)

2. 代码中的嵌套场景

  • Shape 实例的 status 属性为 Ref<STATUS>
  • 当实例被存入 Ref<HotareaVisible<Line>[]> 数组时,Vue 2.7 会递归解包其属性,导致 status 失去 Ref 特性。

解决方案

方案 1:使用 shallowRef 包裹容器

通过浅层响应式容器避免嵌套 Ref 被自动解包。

实现步骤

  1. 修改容器定义ref([]) 替换为 shallowRef([])

    import { shallowRef } from 'vue';
    const areaLines = shallowRef<HotareaVisible<Line>[]>([]);
    
  2. 保留原有 status 定义Shape 类无需修改:

    export abstract class Shape {
      status?: Ref<STATUS> = ref(STATUS.NONE);
    }
    

原理与验证

  • shallowRef 特性:仅追踪 .value 的变化,不递归处理内部对象。

  • 验证示例

    class Shape {
      status = ref(0);
    }
    const container = shallowRef([new Shape()]);
    console.log(container.value[0].status); // 输出 Ref 对象(未被解包)
    

适用场景

  • 需保留现有 Ref 结构,且容器仅需浅层响应。
  • 适用于大型数组/对象容器,避免深度解包的性能损耗。

方案 2:使用 reactive 替代 Ref

通过响应式对象避免嵌套解包问题,提升状态稳定性。

实现步骤

  1. 重构 status 为响应式对象

    export abstract class Shape {
      status = reactive({ value: STATUS.NONE });
    
      constructor(options) {
        // 监听属性值变化
        watch(() => this.status.value, (newVal) => {
          console.log("Status changed:", newVal);
        });
      }
    }
    
  2. 操作状态时直接修改属性

    const shape = new Shape();
    shape.status.value = STATUS.UPDATED; // 触发响应式更新
    

原理与优势

  • reactive 的稳定性:对象属性不会被自动解包,嵌套在 Ref 中时仍保持结构。
  • 明确依赖追踪:通过函数式监听 (() => this.status.value) 精准追踪变化。

适用场景

  • 复杂项目需长期维护,避免隐式解包风险。
  • 状态需要与模板或其他组件深度交互。

总结与最佳实践

以上两种方案都能解决类里面 ref 被解包问题,浅层 ref 确实可以能解决深层被解包问题,但由于项目复杂性,不确定在哪一步又被解包了,再加上浅 ref 监听也是个问题,所以还是采用了reactive 代替了 ref,快速且稳定搞定了该问题。

  1. 理解自动解包机制
  2. 优先选择 reactive
  3. 合理使用 shallowRef
  4. 升级验证策略
Last Updated 2025/4/23 16:13:11