深入理解 Vue 3 的 ref 和 reactive:响应式底层差异与选择策略

深入理解 Vue 3 的 ref 和 reactive:响应式底层差异与选择策略

Vue 3 的响应式系统比 Vue 2 强了很多,但 refreactive 到底用哪个、什么时候用、为什么有时候响应式会"失效",这些问题如果不清楚,写出来的代码很可能踩坑。

我之前也模棱两可,后来花时间看了源码,理解了底层逻辑,现在写代码心里踏实很多。

先说结论

如果你只想知道怎么选:

  • 基本类型(string、number、boolean)用 ref
  • 对象、数组用 reactive
  • 组合式函数(composable)的返回值用 ref(方便解构)
  • 深度响应式需求用 reactive

但知道怎么选不够,你还得知道为什么。

ref 和 reactive 的底层实现差异

reactive 的实现

reactive 底层用的是 Proxy。当你这样写时:

const state = reactive({ count: 0 })

Vue 3 实际做的是:

state = new Proxy({ count: 0 }, {
  get(target, key, receiver) {
    track(target, 'get', key)  // 依赖收集
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver)
    trigger(target, 'set', key)  // 触发更新
    return result
  }
})

Proxy 可以拦截对象的所有操作(get、set、deleteProperty 等),所以对整个对象的任何修改都能被捕获。

ref 的实现

ref 底层其实是一个对象,包含一个 value 属性:

const count = ref(0)

// 展开大概是这个样子
const count = {
  value: 0,
  _isRef: true,
}

Vue 对 ref 的处理比较特殊:

  1. 当你在模板里访问 {{ count }} 时,Vue 自动帮你解开 .value
  2. 当你在 reactive 对象里放一个 ref 时,Vue 也会自动解开

所以 ref 本质上是为了解决基本类型的响应式问题——因为 Proxy 只能代理对象,不能代理基本类型。

响应式失效的典型场景

理解了底层,就能明白为什么会有这些常见问题了。

问题一:解构 reactive 对象后丢失响应式

const state = reactive({ count: 0, name: '张三' })

// 这样解构会丢失响应式
const { count, name } = state

console.log(count) // 0,但不再是响应式的
count++            // 视图不会更新!

为什么?因为 countname 只是普通的常量,值已经被提取出来了,Proxy 的拦截对它们无效。

解决方案:用 toRefs

const state = reactive({ count: 0, name: '张三' })

// 这样解构保持响应式
const { count, name } = toRefs(state)

// count 和 name 现在是 ref,修改时依然会触发更新
count.value++

问题二:reactive 数组直接赋值整个数组

const list = reactive([1, 2, 3])

// 这样赋值会丢失响应式
list = reactive([4, 5, 6])  // ERROR: Assignment to constant

// 正确做法
list.length = 0  // 清空
list.push(4, 5, 6)  // 添加新元素

其实 reactive 数组你一般不会重新赋值整个数组,但如果是 reactive 的对象:

const state = reactive({ items: [1, 2, 3] })

// 整个替换 items 数组
state.items = [4, 5, 6]  // 这是可以的!Proxy 捕获了 set 操作

问题三:reactive 嵌套对象内部新增属性

const state = reactive({ profile: { name: '张三' } })

// 新增一个属性
state.profile.age = 20  // age 不会是响应式的!

因为 Vue 3 的 reactive 只对已有的属性创建响应式追踪。新增的属性需要用 set 或者一开始就把所有属性定义好。

正确做法:

// 方案一:一开始就定义完整
const state = reactive({ profile: { name: '张三', age: undefined } })

// 方案二:使用 ref
const profile = ref({ name: '张三' })
profile.value.age = 20  // ref 的 value 是响应式的

// 方案三:使用 Vue 3.5+ 的浅响应式配合 deep
import { reactive, isRef } from 'vue'

const state = reactive({
  profile: {
    name: '张三',
    // Vue 3.5+ 可以直接这样写
    age: $ref(0)  // 或者用 reactive 包裹
  }
})

ref 和 reactive 的选择策略

什么时候用 ref

1. 基本类型必须用 ref

const count = ref(0)
const name = ref('')
const isLoading = ref(false)

2. 组合式函数的返回值应该用 ref

function useCounter() {
  const count = ref(0)

  function increment() {
    count.value++
  }

  // 返回 ref,用户可以解构使用
  return { count, increment }
}

// 调用方可以这样用
const { count, increment } = useCounter()

3. 需要控制响应式开启/关闭时用 ref

const shouldWatch = ref(true)

watchEffect(() => {
  if (shouldWatch.value) {
    console.log(state.count) // 只有 shouldWatch 为 true 时才收集依赖
  }
})

什么时候用 reactive

1. 有一组相关的状态,用对象组织更清晰时

const formState = reactive({
  username: '',
  password: '',
  rememberMe: false,
})

2. 需要对整个对象做响应式代理时

const tableConfig = reactive({
  columns: ['name', 'status', 'actions'],
  sortable: true,
  filterable: true,
  pageSize: 20,
})

ref 和 reactive 混用的场景

最常见的是在 reactive 对象里放 ref:

const state = reactive({
  count: ref(0),  // ref 会自动解开,但保持响应式
})

// 这样访问和修改
console.log(state.count)      // 自动解开,直接是 0
state.count++                 // 直接操作,不需要 .value

// 但注意,如果是嵌套的 ref,还是要 .value
const outer = ref({ inner: ref(0) })
console.log(outer.value.inner.value)  // 必须用 .value 两次

性能方面的考虑

ref 的开销

每次访问 ref 的 .value 都有一点开销(虽然是纳秒级)。如果你有这样的场景:

// 在一个循环里频繁访问
for (let i = 0; i < 100000; i++) {
  process(state.count)  // 每次都要 .value
}

可以先用局部变量暂存:

const countValue = state.count
for (let i = 0; i < 100000; i++) {
  process(countValue)  // 更快
}

reactive 的深度代理开销

reactive 会对对象进行深度代理。如果你的对象树很深很大,初始化时会慢一些。另外,每次访问深层属性也会多走几层 Proxy 链。

对于超大型数据结构,可以考虑用 shallowReactive

const state = shallowReactive({
  // 只有第一层是响应式的
  deep: {
    nested: {
      value: 1  // 这里不是响应式的
    }
  }
})

但用了 shallowReactive 就意味着深层修改不会触发更新,要根据业务场景选择。

实际项目中的最佳实践

综合以上分析,我在实际项目中这样用:

// 1. 基本类型用 ref
const isLoading = ref(false)
const currentId = ref<string | null>(null)

// 2. 相关联的状态用 reactive 对象
const filterState = reactive({
  keyword: '',
  status: '',
  dateRange: [] as string[],
})

// 3. 表单场景用 reactive + ref 混合
const formState = reactive({
  username: '',
  password: '',
})

const { username, password } = toRefs(formState)

// 4. 组合式函数返回 ref
export function usePagination() {
  const page = ref(1)
  const pageSize = ref(20)
  const total = ref(0)

  function nextPage() {
    page.value++
  }

  return {
    page,
    pageSize,
    total,
    nextPage,
  }
}

// 5. 需要深度响应式的大型对象用 reactive
const globalState = reactive({
  user: {
    id: '',
    name: '',
    permissions: [] as string[],
  },
  settings: {
    theme: 'light',
    language: 'zh-CN',
  },
})

总结

理解 ref 和 reactive 的底层差异,能帮你在写代码时做出更正确的选择:

  1. 基本类型用 ref,对象/数组用 reactive——这是最基本的原则
  2. reactive 对象解构要用 toRefs——否则丢失响应式
  3. reactive 只对已有属性创建响应式——新增属性要考虑
  4. 组合式函数返回 ref——方便调用方解构使用
  5. 超大型对象考虑 shallowReactive——减少初始化开销

写 Vue 3 代码时多思考响应式边界这些问题,上线后能少很多莫名其妙的 bug。

最后更新 4/20/2026, 6:02:32 AM