vue中ref与reactive

2025-10-08
21848 分钟
...

Vue 3 的 refreactive:区别、实现,以及为什么更推荐 ref

1. 一句话结论

  • ref:适合原始类型独立、细粒度的状态单元;类型推断明确、解构安全、追踪粒度更小,默认首选
  • reactive:适合管理对象/集合这类结构化状态;但解构会丢失响应性watch 默认非深度,需要正确的使用姿势配合。

2. 用法与语义差异

2.1 基本示例

import { ref, reactive } from 'vue';

// ref:包装任何值(常见是原始类型)
const count = ref(0);
count.value++; // 必须通过 .value

// reactive:把对象/数组“代理化”
const state = reactive({ a: 1, nested: { b: 2 } });
state.a++;           // 直接改
state.nested.b++;    // 深层也响应(非 shallowReactive)

2.2 模板中的解包(unwrapping)

  • 模板 里,ref自动解包,可直接写 {{ count }},不必 .value

  • 在响应式对象中reactive(obj) 之后的对象),内部的 ref 属性会被解包为其 .value(除 shallowReactive):

    const r = reactive({ n: ref(1) });
    console.log(r.n); // 1(已解包)
    r.n = 2;          // 等价于给原 ref 赋值 2
  • 数组或 Map/Set 里的 ref 不会被自动解包(避免语义混乱)。

2.3 解构陷阱

  • reactive解构/展开失去响应性

    const s = reactive({ a: 1 });
    const { a } = s;   // ❌ a 与原对象断联

    解决:用 toRefs

    import { toRefs } from 'vue';
    const { a } = toRefs(s); // ✅ a 是一个 ref
  • ref 解构安全:解构出来的仍是 ref 本身(带 .value)。

2.4 watch 行为差别

import { watch } from 'vue';

watch(count, (v) => { /* 触发:count.value 改变 */ });

watch(state, (v) => { /* 不会因为 state.a 改变而触发(非深度)*/ });

watch(() => state.a, (v) => { /* 触发:state.a 改变 */ });
watch(state, (v) => { /* 深度监听 */ }, { deep: true }); // 成本更高
  • reactive 根对象做 watch,默认非深度。更推荐侦听具体字段使用 toRef/toRefs

3. 内部实现原理(精简但够用)

3.1 总览:依赖收集与触发

Vue 3 基于 effect/依赖图 + track/trigger

  • track(target, key):在读取阶段收集“谁用到了哪个 key”。
  • trigger(target, key):在写入阶段触发与该 key 相关的副作用。

3.2 reactive 的实现要点(Proxy)

  • 核心是 ES Proxy 包装对象,对 get/set 等拦截:

    const proxy = new Proxy(target, {
      get(target, key, receiver) {
        track(target, key);                 // 读取时收集依赖
        const res = Reflect.get(target, key, receiver);
        // 深层对象懒代理(返回子对象时再做代理)
        return isObject(res) ? reactive(res) : res;
      },
      set(target, key, value, receiver) {
        const old = (target as any)[key];
        const ok = Reflect.set(target, key, value, receiver);
        if (ok && value !== old) trigger(target, key); // 定向触发
        return ok;
      }
    });
  • 优点:天然支持深层响应、对对象/数组的操作都能被追踪。

  • 成本:读取任何属性都会 track,对象越大、层级越深,追踪图越复杂;深度监听(deep: true)开销明显。

3.3 ref 的实现要点(RefImpl)

  • 本质是一个带 getter/setter 的壳:

    class RefImpl<T> {
      private _value: T;
      dep = new Dep(); // 与这个 ref 绑定的依赖集合
    
      get value() {
        trackRefValue(this);      // 读取时收集依赖(以“这个 ref”为粒度)
        return this._value;
      }
      set value(next: T) {
        if (!Object.is(next, this._value)) {
          this._value = next;
          triggerRefValue(this);  // 触发所有依赖这个 ref 的 effect
        }
      }
    }
  • 粒度更细:依赖挂在“这个 ref 实例”上,而不是“某个对象上的 key”。对比 reactive依赖边更少、更可控


4. 性能与可维护性对比(为什么更推荐 ref

4.1 追踪粒度与稳定性

  • ref每个 ref 一条独立依赖线,粒度最细;修改只触发依赖它的副作用。
  • reactive:在对象级别做依赖图,容易“过度追踪”(读到多少 key 就 track 多少 key);深层对象的变动传播路径更复杂。

结果:小而独立的状态用 ref渲染更可预期、无额外噪音更新

4.2 类型推断与解构

  • ref<T> 在 TS 中表现清晰:使用时总是 .value;团队阅读/重构更直观
  • reactive 解构会断开响应;需要 toRefs,API 复杂度上升。

4.3 组合式复用

  • 组合式函数(composables)对外暴露一组 ref,调用方易于挑选/重命名/解构,不担心丢响应。
  • 库代码中暴露 ref 也更**“自描述”**:这是“状态单元”,不是随意可变的大对象。

5. 什么时候仍然用 reactive

  • 模型型/表单型状态:天然是对象结构,字段多、需要一起传来传去。

  • 需要深层响应且不想手动为每个字段造 ref

  • 集合/增删改频繁的对象/数组(如表格数据),配合 reactive([])reactive(new Map()) 直观好用。

    小技巧:对数组的大改动,可用 arr.splice 等原生方法,触发行为最可靠。


6. 常见 API 及坑位清单

6.1 相关 API

  • shallowRef / shallowReactive:只追踪顶层,不做深层代理/追踪。适合大对象(如第三方实例)或频繁整体替换的场景。
  • toRef(obj, key) / toRefs(obj):从 reactive 对象生成可解构的 ref
  • unref(x) / isRef(x):工具化判断与剥壳。
  • computed:产出只读 ref(可 set 的 computed 是带 setter 的变体)。

6.2 常见坑

  1. reactive 解构丢响应 → 用 toRefs
  2. 数组中的 ref 不自动解包 → 渲染时 item.value,或前置 unref
  3. watch(reactiveObj) 不触发 → 侦听具体 getter:watch(() => obj.a, ...),或加 { deep: true }(慎用)。
  4. props 取值后解构const { title } = toRefs(props),保持响应。
  5. 与第三方不可代理对象(如 DOM、类实例) → 用 shallowRef 存,整体替换。

7. 推荐的实践模式

7.1 默认使用 ref

import { ref, computed, watch } from 'vue';

// ✅ 小状态用 ref
const loading = ref(false);
const page = ref(1);
const keyword = ref('');

// ✅ computed / watch 都以 ref 为单位
const canSearch = computed(() => keyword.value.trim().length > 0);
watch(page, () => {
  /* fetchList(page.value) */
});

7.2 需要对象时,用 reactive + toRefs 暴露

import { reactive, toRefs, computed } from 'vue';

function useForm() {
  const form = reactive({
    username: '',
    password: '',
    remember: false,
  });

  const valid = computed(() => form.username !== '' && form.password !== '');
  return { ...toRefs(form), valid }; // 对外全是 ref,调用方解构安全
}

7.3 大对象/实例用 shallowRef

import { shallowRef, onMounted } from 'vue';

type Chart = any; // 替换为你的图表类型

const chart = shallowRef<Chart | null>(null);

onMounted(() => {
  chart.value = /* new Chart(...) */ null;
});

function reset() {
  chart.value?.destroy?.();
  chart.value = null;
}

7.4 列表/表格数据(按需细化)

import { reactive } from 'vue';

// 行内交互状态细分为字段,避免整表刷新
const rows = reactive<{ id: number; checked: boolean }[]>([]);

// 或者把“是否选中”等独立成 Map<id, boolean>
const checkedMap = reactive(new Map<number, boolean>());

8. 更深入的实现差异(针对性能感知)

  • 依赖键的维度

    • ref以“实例”为键(单槽位依赖)。
    • reactive以“目标对象 + 属性 key” 为键(键数多时,依赖集合膨胀更快)。
  • 读取时机

    • reactive.get 每次读取属性都会 track
    • ref.value 只在读取 .valuetrack对模板/渲染更友好(模板会只读取使用到的那些 ref)。
  • 深层对象的惰性代理

    • reactive 对子对象是惰性代理:访问到子对象时才 reactive 一次。但访问路径多时,仍会增加代理层与追踪点。

实战经验:refref,能“按字段追踪”就别“整对象追踪深度变化”

9. 选型清单(速查)

需求/场景建议
原始类型、小状态ref
字段很多的表单模型reactive + 导出 toRefs
需要整体替换的大对象/第三方实例shallowRef
深层结构频繁更新reactive(必要时局部字段拆成 ref
侦听变化watch(ref)watch(() => obj.key),尽量避免 deep: true

10. 小结:为什么更推荐 ref

  1. 更细的依赖粒度 → 更少无关更新、渲染更可控。
  2. 类型与语义更直观.value 明确表达“这是响应式单元”。
  3. 解构安全 → 组合式 API 中拆解/重命名无副作用。
  4. 更契合组合式设计 → composable 对外暴露 ref,复用边界清晰。

实践准则默认用 ref,遇到结构化/深层状态再引入 reactive,并用 toRefs 保持边界清晰

11. 附:常用代码片段

11.1 从 reactive 派生出字段级 ref

import { reactive, toRef } from 'vue';

const user = reactive({ name: 'hua', age: 20 });
const name = toRef(user, 'name');  // ref<string>
name.value = '张三';                // 同步回 user.name

11.2 安全暴露组合式状态

import { ref } from 'vue';

export function useCounter() {
  const n = ref(0);
  const inc = () => n.value++;
  const reset = () => (n.value = 0);
  return { n, inc, reset }; // 外部拿到的都是 ref/函数
}

11.3 深层监听的替代写法

import { watch } from 'vue';

// 不用 deep: true,而是精确侦听需要的字段
watch(() => state.filters.keyword, (kw) => {
  /* doSearch(kw) */
}, { /* 可配 debounce/throttle */ });

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
vue2025-10-22
shallowRef 与 ref 的区别、场景与坑位
结论先行: 会 深度 地把你放进去的对象转成响应式(递归代理),因此"对象内部的改动"也会触发视图更新。 只在 顶层 做依赖追踪: 更换 才触发更新 ;若你只是"在原对象上改属性",不会触发,除非手动 。 1\. 快速对比 维度 追踪粒度 深度(递归) 仅顶层 对象内部属性变更 会触发更新 不会 触发(除非 或重新赋值) 性能 有递归 & 依赖跟踪开销 更轻量,适合大对象/第三方实例 适用对象 普通标量、普通对象、数组 第三方实例(图表...
学习面试
vue2025-10-20
Vue 3 中的 setup执行时机与异步逻辑详解
Vue 3 中的 执行时机与异步逻辑详解 本文详细介绍 Vue 3 组合式 API 的核心函数 :它在组件生命周期中的执行时机、异步行为( 的影响)、常见用法和最佳实践。 一、 的执行时机 是 Vue 3 组件实例创建后、渲染前执行的函数。 它相当于 Vue 2 中的 和 的合体。 执行流程: 也就是说: 所有响应式数据( 、 、 )都在这里定义; 所有生命周期钩子( 、 等)都在这里注册; 模板渲染会等待 执行完成(或异步返回的 Pr...
学习面试
vue2025-09-23
Vue3 中computed属性依赖总结
Vue3 属性依赖总结 1. 依赖收集机制 的 getter 内部 读取 到哪些响应式变量 ( / ),就会把这些变量收集为依赖。 当这些依赖发生变化时, 会重新计算。 2. 如果只返回常量 始终是 。 修改 或 不会触发 重新计算,因为 getter 内没有访问它们。 3. 如果访问了响应式变量但仍返回常量 初始化时会打印一次。 当 或 改变时,getter 会重新执行(再次打印),因为它们被收集为依赖。 但 依旧始终是 。 4. 总...
学习面试
vue2025-09-17
template标签为什么不可以使用v-show
前言 写 Vue 时,最容易踩的小坑之一,就是给 套一个 。编辑器不报错,页面却“消失”了。很多人以为这是 bug,其实它只是 Vue 的底层设计在默默提醒: 不是真节点,它只是个“草稿纸”。 一、为什么 拒绝 v show 的本质是切换元素的 CSS: 。浏览器要找到真实 DOM 节点,才能给它加样式。而 在运行时会被完全剥离,不会留下任何标签,Vue 找不到节点,也就无处下刀。 举个反面教材: 打开控制台,你会看到 还是生成了,但...
学习面试
vue2025-09-17
Vue 3中watchEffect自动追踪详解
Vue 3 自动追踪详解 一、是什么 是 Vue 3 Composition API 提供的一个响应式副作用(side effect)函数。 它会 立即执行一次回调函数 ,并且在回调函数中访问到的所有响应式数据(ref、reactive、computed 等)都会被 自动追踪 。 当这些依赖的响应式数据发生变化时,回调会重新执行。 简单理解: 能“自动发现”它用到的响应式变量,不需要手动指定依赖。 二、基本用法 特点: 第一次立即执行...
学习面试
vue2025-07-17
vue3源码解析:ref原理
今天我们来看看 ref 的具体实现。ref 是 Vue 3 中最基础的响应式 API 之一,它可以将任何值转换为响应式对象。 实例引入 首先通过一个例子来看看 ref 的用法: 这个例子展示了 ref 的基本使用: 1. 通过 ref() 创建响应式引用 2. 通过 .value 访问和修改值 3. 在模板中会自动解包,不需要 .value 核心数据结构 下面通过源码来看看 ref 的核心数据结构和方法: 1\. 类型系统设计 首先看看...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录