Vue3里computed和reactive咋选?一篇讲透区别、原理和实战
很多刚开始玩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)等操作,举个流程:
- 当你用
reactive({...})把对象包起来,Vue会给这个对象做一层Proxy代理。 - 组件渲染时,只要访问了reactive对象的属性(比如
user.firstName),Proxy的get陷阱就会把“当前组件的渲染函数”加入依赖集合(记下来:“这个组件用了firstName,以后firstName变了要通知它”)。 - 当你修改属性(比如
user.firstName = '李'),Proxy的set陷阱会触发——找到所有依赖这个属性的组件/计算属性,通知它们“数据变了,重新渲染/计算”。
而且reactive是深层响应式,对象里嵌套的对象、数组,都会被Proxy递归处理,保证层层修改都能触发更新~
computed的原理:依赖追踪+缓存(dirty标记)
computed的核心是“懒计算+缓存”,原理分三步:
- 依赖收集:computed的回调函数里,肯定会访问其他响应式数据(比如reactive里的属性),这时候,Vue会把computed自己注册为这些数据的“依赖”(相当于告诉reactive:“我用了你的数据,以后你变了要通知我”)。
- 缓存标记(dirty):computed有个内部标记叫
dirty(脏了没),第一次访问computed的值时,会执行回调计算结果,存起来,然后把dirty设为false(表示“结果新鲜,下次不用重新算”)。 - 触发更新:当computed依赖的响应式数据变化时,Vue会把
dirty设为true(表示“结果脏了,下次访问要重新算”),等下次再访问computed的值时,才会重新执行回调计算,更新结果后再把dirty设为false。
这么设计是为了减少不必要的计算——比如一个组件里的computed,就算渲染10次,只要依赖没变化,computed只需要计算1次,剩下9次直接拿缓存结果~
实战中,啥场景必须用computed,啥时候适合reactive?
得根据“数据的角色”和“业务逻辑”选工具,选错了要么逻辑乱,要么性能差~
必须用computed的3种场景
-
场景1:基于其他响应式数据做“实时推导”
比如购物车列表是reactive的数组,每个商品有price和count,总价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里的price或count变了,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 })正确姿势:用
onMounted或watch处理异步,再把结果存在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前端网发表,如需转载,请注明页面地址。
code前端网



