Code前端首页关于Code前端联系我们

Vue3里computed和reactive咋选?一篇讲透区别、原理和实战

terry 3天前 阅读数 396 #SEO

很多刚开始玩Vue3的小伙伴,面对computed和reactive经常犯迷糊——这俩都和响应式有关,到底有啥区别?啥时候该用哪个?今天咱就从功能、原理、场景、避坑这些角度,把这俩工具掰明白~

computed和reactive最核心的功能区别是啥?

你可以把reactive理解成“响应式数据容器”,负责把对象/数组包成“改了能自动触发视图更新”的结构;而computed是“响应式数据推导器+缓存器”,专门基于其他响应式数据做计算,还能记住上次结果,避免重复计算。

举个🌰:做用户信息展示时,

  • 用reactive存原始数据:
    const user = reactive({ firstName: '张', lastName: '三' })
    user.firstName = '李' // 改了之后,用了user的组件会自动更新
  • 用computed推导“全名”:
    const fullName = computed(() => user.firstName + user.lastName)
    console.log(fullName.value) // 当user的firstName/lastName变了,fullName自动变

    简单说:reactive是“存数据的盒子”,computed是“基于盒子里的数据做计算的智能计算器”~

从底层原理看,它们咋实现响应式?

这就得聊Vue3的响应式核心机制了,俩工具原理差异还挺大:

reactive的原理:Proxy拦截+依赖收集

reactive靠ES6的Proxy实现,Proxy能“拦截”对象的读取(get)、修改(set)等操作,举个流程:

  1. 当你用reactive({...})把对象包起来,Vue会给这个对象做一层Proxy代理。
  2. 组件渲染时,只要访问了reactive对象的属性(比如user.firstName),Proxy的get陷阱就会把“当前组件的渲染函数”加入依赖集合(记下来:“这个组件用了firstName,以后firstName变了要通知它”)。
  3. 当你修改属性(比如user.firstName = '李'),Proxy的set陷阱会触发——找到所有依赖这个属性的组件/计算属性,通知它们“数据变了,重新渲染/计算”。

而且reactive是深层响应式,对象里嵌套的对象、数组,都会被Proxy递归处理,保证层层修改都能触发更新~

computed的原理:依赖追踪+缓存(dirty标记)

computed的核心是“懒计算+缓存”,原理分三步:

  1. 依赖收集:computed的回调函数里,肯定会访问其他响应式数据(比如reactive里的属性),这时候,Vue会把computed自己注册为这些数据的“依赖”(相当于告诉reactive:“我用了你的数据,以后你变了要通知我”)。
  2. 缓存标记(dirty):computed有个内部标记叫dirty(脏了没),第一次访问computed的值时,会执行回调计算结果,存起来,然后把dirty设为false(表示“结果新鲜,下次不用重新算”)。
  3. 触发更新:当computed依赖的响应式数据变化时,Vue会把dirty设为true(表示“结果脏了,下次访问要重新算”),等下次再访问computed的值时,才会重新执行回调计算,更新结果后再把dirty设为false

这么设计是为了减少不必要的计算——比如一个组件里的computed,就算渲染10次,只要依赖没变化,computed只需要计算1次,剩下9次直接拿缓存结果~

实战中,啥场景必须用computed,啥时候适合reactive?

得根据“数据的角色”和“业务逻辑”选工具,选错了要么逻辑乱,要么性能差~

必须用computed的3种场景

  • 场景1:基于其他响应式数据做“实时推导”
    比如购物车列表是reactive的数组,每个商品有pricecount,总价total得是price*count的累加,这时候用computed,依赖商品列表变化自动更新:

    const cart = reactive({
      items: [
        { id: 1, price: 50, count: 2 },
        { id: 2, price: 10, count: 3 }
      ]
    })
    const totalPrice = computed(() => {
      return cart.items.reduce((sum, item) => sum + item.price * item.count, 0)
    })

    只要cart.items里的pricecount变了,totalPrice自动重新计算~

  • 场景2:需要“缓存计算结果,避免重复消耗”
    比如一个复杂的筛选逻辑(比如根据关键词+分类+价格区间,从几百条数据里筛结果),如果每次组件渲染都重新跑筛选函数,性能会很差,用computed的话,只有当关键词/分类/价格区间这些依赖变化时,才会重新筛选,否则复用上次结果:

    const filterParams = reactive({ keyword: '', category: 'all', priceRange: [0, 9999] })
    const filteredList = computed(() => {
      return bigList.filter(item => {
        // 复杂的筛选逻辑...
      })
    })
  • 场景3:需要“双向绑定的计算属性(带setter)”
    比如用户输入“全名”,要自动拆分成“姓”和“名”存到reactive对象里,这时候computed可以写setter:

    const user = reactive({ firstName: '孙', lastName: '悟空' })
    const fullName = computed({
      get() { return user.lastName + user.firstName },
      set(val) { 
        const [last, first] = val.split(' ') // 假设输入格式是“姓 名”
        user.lastName = last
        user.firstName = first
      }
    })
    // 模板里双向绑定:<input v-model="fullName" />

适合用reactive的2种场景

  • 场景1:管理“复杂对象/数组的响应式状态”
    比如做后台管理系统的“用户详情页”,用户信息包含头像、个人资料、权限等嵌套结构,用reactive包起来,修改任何层级的属性都能触发更新:

    const user = reactive({
      info: { name: '八戒', age: 18 },
      roles: ['user', 'vip'],
      settings: { theme: 'dark', lang: 'zh' }
    })
    user.settings.theme = 'light' // 改了会触发视图更新
  • 场景2:封装“组件内部的多属性联动状态”
    比如表单里有“用户名、密码、确认密码、手机号”多个输入项,用reactive把它们整合成一个对象,方便统一管理和传递:

    const formState = reactive({
      username: '',
      password: '',
      confirmPwd: '',
      phone: ''
    })
    // 提交时直接传formState,验证逻辑也基于这个对象写
    function onSubmit() {
      if (formState.password !== formState.confirmPwd) { /* 提示 */ }
      // ...
    }

用这俩工具时,最容易踩的坑是啥?

踩过坑才知道:工具用不对,代码debug到崩溃…分享几个高频踩坑点:

reactive的3个典型坑

  • 坑1:reactive不能包“基本类型”(字符串、数字、布尔)
    比如想做个响应式的计数器,写成const count = reactive(0)完全没用!因为Proxy只能拦截对象/数组的操作,基本类型没法被Proxy代理。正确姿势是用ref

    const count = ref(0)
    count.value++ // 这样才会触发响应式更新
  • 坑2:解构赋值会“断开响应式”
    比如从reactive对象里解构属性:

    const user = reactive({ name: '沙僧', age: 20 })
    const { name } = user // 这里name变成普通字符串,和user.name断开关联
    user.name = '悟净' // 改user.name,name变量不会更新!

    解决方法:用toRefs把reactive对象转成“每个属性都是ref”的结构,再解构:

    import { toRefs } from 'vue'
    const { name, age } = toRefs(user)
    console.log(name.value) // 现在name是ref,响应式还在
  • 坑3:数组操作要注意“响应式触发”
    reactive的数组虽然是响应式的,但像arr[0] = newVal 或者 delete arr[0] 这种操作,Vue3默认不触发更新(因为Proxy对这些操作的拦截有限)。正确姿势:用Vue提供的数组方法(push、pop、splice等),或者用ref包裹数组:

    const list = reactive([1, 2, 3])
    // 错误:直接改索引,不触发更新
    list[0] = 100 
    // 正确:用splice
    list.splice(0, 1, 100) 
    // 或者用ref包裹数组
    const list = ref([1, 2, 3])
    list.value[0] = 100 // ref的.value是数组,修改会触发更新

computed的3个典型坑

  • 坑1:getter里不能有“副作用”
    computed的回调(getter)应该是纯函数——只做计算,不能发请求、修改其他响应式数据、操作DOM…否则会导致依赖关系混乱,甚至死循环,比如这样写就错了:

    const user = reactive({ name: '' })
    const info = computed(() => {
      // 错误:在computed里发请求(副作用)
      fetch('/api/user').then(res => {
        user.name = res.name // 这里修改其他数据,会搞乱依赖
      })
      return user.name
    })

    正确姿势:用onMountedwatch处理异步,再把结果存在reactive/ref里:

    onMounted(() => {
      fetch('/api/user').then(res => {
        user.name = res.name
      })
    })
    const info = computed(() => user.name)
  • 坑2:依赖的响应式数据没“正确关联”
    computed的回调里必须访问响应式数据(reactive/ref/computed包过的),否则就算数据变了,computed也不会更新,比如这样写:

    let普通变量 = '普通字符串'
    const wrongComputed = computed(() => 普通变量 + '后缀')
    普通变量 = '新字符串' // wrongComputed不会更新!

    解决方法:把普通变量用ref包起来,让computed能追踪到依赖:

    const ref变量 = ref('普通字符串')
    const rightComputed = computed(() => ref变量.value + '后缀')
    ref变量.value = '新字符串' // rightComputed会更新
  • 坑3:误把computed当“方法”用
    computed在模板里是属性,不是方法!如果写成{{ fullName() }},每次组件渲染都会执行函数,完全失去缓存的意义。正确写法是直接用属性:

    // 错误:当方法用
    const fullName = computed(() => user.firstName + user.lastName)
    <div>{{ fullName() }}</div> 
    // 正确:当属性用
    <div>{{ fullName }}</div> 

性能角度,computed和reactive怎么选?

性能优化得看“场景需求”和“工具特性”:

computed的性能优势:缓存减少重复计算

如果一个计算逻辑依赖的数据源不常变化,用computed能省性能,比如页面上的“统计数值”(总金额、未读消息数)、“筛选后的列表”,这些逻辑每次重新计算可能很耗时,用computed的缓存机制,只有依赖变了才重新算,否则复用结果。

reactive的性能开销:Proxy的合理使用

reactive靠Proxy实现响应式,理论上有“拦截操作”的开销,但Vue3对Proxy的优化已经很成熟,只要不是极端场景(比如同时创建成千上万个reactive对象),性能问题可以忽略,实际开发中,重点是合理组织数据结构——别把没必要响应式的东西硬塞进reactive里(比如纯展示的静态数据)。

结合场景选工具

  • 如果是“频繁变化的复杂对象状态管理”(比如表单多字段、用户信息嵌套结构),用reactive完全没问题,Vue3的响应式性能扛得住。
  • 如果是“基于已有数据的推导+缓存”(比如总价计算、全名拼接、复杂筛选),必须用computed,既保证数据实时更新,又能省性能。

computed和reactive能配合着用吗?怎么联动更丝滑?

必须能啊!而且实战中经常这么玩,举两个典型例子:

例子1:用户信息的“原始数据+推导数据”联动

做用户信息编辑页时,用reactive存原始数据,用computed做推导和双向绑定:

const user = reactive({
  firstName: '唐',
  lastName: '三藏',
  birthday: '600-01-01' // 假设后端返回生日字符串
})
// 用computed推导年龄(根据birthday计算)
const age = computed(() => {
  const birthYear = new Date(user.birthday).getFullYear()
  return new Date().getFullYear() - birthYear
})
// 用computed推导全名,还支持双向绑定(setter)
const fullName = computed({
  get() { return user.lastName + user.firstName },
  set(val) { 
    const [last, first] = val.split(' ') // 假设输入格式“姓 名”
    user.lastName = last
    user.firstName = first
  }
})
// 模板里这样用:
<input v-model="fullName" placeholder="请输入姓名(格式:姓 名)" />
<p>年龄:{{ age }}</p>

这里reactive管原始数据,computed做“年龄计算”和“全名双向绑定”,逻辑分层清晰,维护起来超方便~

例子2:购物车的“商品列表+总价计算”联动

电商项目里的购物车,用reactive存商品列表,computed算总价:

const cart = reactive({
  items: [
    { id: 1, name: '经卷', price: 100, count: 2 },
    { id: 2, name: '禅杖', price: 200, count: 1 }
  ]
})
// computed算总价
const totalPrice = computed(() => {
  return cart.items.reduce((sum, item) => sum + item.price * item.count, 0)
})
// 点击按钮修改商品数量,totalPrice自动更新
function addCount(item) {
  item.count++
}
// 模板里展示:
<ul>
  <li v-for="item in cart.items" :key="item.id">
    {{ item.name }} × {{ item.count }} ¥{{ item.price }}
    <button @click="addCount(item)">+</button>
  </li>
</ul>
<p>总价:¥{{ totalPrice }}</p>

只要cart.items里的count或price变化,totalPrice会自动重新计算,而且computed的缓存机制避免了每次渲染都执行reduce,性能很稳~

和ref比起来,computed、reactive在响应式里的角色有啥不同?

简单总结三者的分工:

  • ref:专门处理基本类型(字符串、数字、布尔等)的响应式,通过.value访问值,适合单个独立的值(比如计数器、开关状态)。
  • reactive:专门处理对象/数组的响应式,直接访问属性,适合复杂数据结构(比如用户信息、购物车列表)。
  • computed:基于ref/reactive的数据做计算+缓存,输出的也是响应式数据,适合“推导逻辑”(比如全名、总价、筛选结果)。

关系像“容器”和“加工机”:ref和reactive是“存数据的容器”,computed是“基于容器里的数据做加工的机器”~

最后总结一下

  • 管理对象/数组的响应式状态,选reactive;
  • 基于已有响应式数据做推导+缓存,选computed;
  • 用的时候注意避坑(比如reactive别包基本类型、computed别写副作用);
  • 两者还能配合着用,让数据流动更丝滑~

其实理解透它们的“角色”和“原理”,选的时候就不会纠结啦

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门