Vue2里的computed到底怎么用?和methods、watch有啥不一样?
不少刚开始学Vue2的同学,提起computed(计算属性)总是又好奇又迷茫:它和methods、watch有啥区别?什么时候必须用它?缓存机制到底咋工作的?这篇文章用问答形式,把computed的核心逻辑、使用场景、避坑技巧全拆明白,哪怕是刚入门的新手,也能一步步搞懂怎么把computed用对、用顺~
computed到底是干啥的?
简单说,computed是Vue里用来基于已有数据动态生成新值的工具,它最大的特点是“依赖缓存”——只有当依赖的数据源变化时,才会重新计算;如果依赖没变化,直接复用之前的计算结果。
举个生活里的例子:你点外卖时,订单总价(computed)依赖菜品单价、数量、优惠折扣(数据源),只要这些数据源不变,总价就不变;一旦某道菜加量、换菜品(数据源变了),总价才会重新算。
再看代码场景:假设要做一个“购物车总价”功能,数据结构长这样👇
data() { return { goods: [ { name: '卫衣', price: 129, count: 2 }, { name: '球鞋', price: 599, count: 1 } ], discount: 0.9 // 9折优惠 } }
要计算最终支付金额,用computed可以这么写:
computed: { totalPrice() { let sum = 0; this.goods.forEach(good => { sum += good.price * good.count; }); return sum * this.discount; } }
模板里直接用{{ totalPrice }}
就行,这时候只要goods
里的price/count
、discount
不变,totalPrice
就不会重复计算;一旦其中任意一个数据变化,Vue会自动重新计算总价。
要是不用computed,直接在模板里写逻辑(比如{{ goods.reduce(...) * discount }}
),不仅模板臃肿难维护,还会每次组件渲染都重复计算——哪怕数据没变化,性能浪费很严重。
computed和methods有啥本质区别?
很多同学最开始会混淆这俩,核心差异在“是否缓存”和“触发时机”上。
(1)缓存机制:computed存结果,methods每次执行
methods里的函数,每次被调用时都会重新执行;而computed会把计算结果“存起来”,只有依赖的数据源变化时,才会重新计算。
比如做一个“当前时间格式化”功能:
// methods写法 methods: { formatTime() { return dayjs().format('YYYY-MM-DD'); } } // computed写法 computed: { formattedTime() { return dayjs().format('YYYY-MM-DD'); } }
模板里如果写{{ formatTime() }}
,每次组件渲染(哪怕数据没变化)都会执行formatTime
函数;但如果写{{ formattedTime }}
,只有当formattedTime
依赖的数据源变化时才会重新计算,但这里有个小陷阱:dayjs()
是“非响应式”数据(不是data里的变量),所以formattedTime
的依赖其实没变化,它会一直返回第一次计算的结果——这也侧面说明computed的缓存是基于“响应式数据源”的(后面会细讲)。
(2)性能差异:循环场景下天差地别
如果在v-for
循环里调用方法,性能问题会被放大,比如渲染100条数据,每条都调用methods里的函数:
<ul> <li v-for="item in list" :key="item.id"> {{ formatItem(item) }} </li> </ul>
只要组件重新渲染(比如父组件传参变化、其他数据更新),formatItem
会被调用100次;但如果用computed先把所有格式化后的结果生成好,再渲染:
computed: { formattedList() { return this.list.map(item => this.formatItem(item)); } }, methods: { formatItem(item) { /* 格式化逻辑 */ } }
模板里用v-for="item in formattedList"
,只有list
变化时,formattedList
才会重新计算,渲染时直接拿缓存结果,性能好太多。
(3)适用场景:一个“算值”,一个“干活”
总结下选择逻辑:
- computed:依赖其他数据推导新值,且希望结果被缓存(比如过滤列表、计算总价、数据格式化)。
- methods:需要主动触发执行(比如点击事件),或不需要缓存的一次性操作(比如表单提交、事件处理函数)。
computed的缓存是怎么工作的?哪些情况会让它重新计算?
理解computed的缓存逻辑,得先懂Vue的“响应式依赖收集”机制:
- 当执行computed的getter函数时,Vue会自动“收集”里面用到的响应式数据(比如data、props里的变量)作为“依赖”。
- 当这些依赖的数据发生变化时,Vue会标记这个computed为“脏”(需要重新计算)。
- 下次访问这个computed属性时,才会真正重新计算,并更新缓存结果。
举个例子:
data() { return { a: 1, b: 2 } }, computed: { sum() { return this.a + this.b; // 收集a和b作为依赖 } }
当a
或b
变化时,sum
会被标记为脏;但如果只是访问sum
,而a
和b
没变化,sum
会直接返回之前缓存的结果。
(1)“非响应式数据”不会触发重新计算
如果computed里用了非响应式数据(比如普通变量、window对象、第三方库的非响应式数据),Vue没法收集到依赖,所以数据变化时computed不会更新。
比如这样写就会踩坑:
let num = 1; // 普通变量,非响应式 data() { return { a: 2 } }, computed: { result() { return num + this.a; // num不是响应式数据 } }
哪怕后面执行num = 3
,result
也不会重新计算——因为Vue没把num
当成依赖,解决方法:把num
放到data里(变成响应式),或者用watch监听num
的变化(但num
如果是外部变量,watch也监听不到,所以最好把需要响应的数放data/props)。
(2)数组/对象的“变异”要注意
Vue2里,数组的索引修改、长度修改(比如arr[0] = 1
、arr.length = 0
)不会触发响应式更新,所以如果computed依赖这样的数组,修改方式不对就不会重新计算。
正确做法是用数组的变异方法(push/pop/splice
等),或者用this.$set
修改对象属性。
data() { return { list: [1, 2, 3] } }, computed: { listSum() { return this.list.reduce((t, v) => t + v, 0); } } // 错误修改:直接改索引 this.list[0] = 10; // listSum不会更新 // 正确修改:用splice this.list.splice(0, 1, 10); // listSum会更新
computed里能写异步操作吗?为啥?
不建议在computed里写异步,甚至可以说“不能正确工作”,原因和computed的设计逻辑有关:
computed的getter函数需要返回一个确定的值,用来做缓存和依赖收集,但异步操作(比如axios请求、setTimeout)的结果是“延迟返回”的,getter执行时拿不到异步结果,只能返回undefined
或者初始值,后续异步完成时,computed的依赖机制也没法感知到变化,导致缓存失效或结果错误。
举个反例:想通过异步请求获取用户信息,然后计算用户名:
computed: { userFullName() { let name = ''; axios.get('/user').then(res => { name = res.data.first + res.data.last; }); return name; // 这里返回的是初始值'',异步结果拿到后也没法更新computed } }
模板里显示{{ userFullName }}
永远是空字符串,因为getter执行时异步还没完成,后续异步完成后,computed也不会重新计算。
那要处理异步场景咋办?推荐用watch + data或者methods:
data() { return { user: null, fullName: '' } }, watch: { // 监听user变化,异步请求后更新fullName user(newVal) { axios.get(`/user/${newVal.id}`).then(res => { this.fullName = res.data.first + res.data.last; }); } }, methods: { async fetchUser() { const res = await axios.get('/currentUser'); this.user = res.data; } }
这样user
变化时,watch触发异步请求,更新fullName
(fullName
可以是普通data,也可以是computed依赖的数据源),逻辑更可控。
computed的setter怎么用?平时用得多吗?
默认情况下,computed只有getter(用来获取值),但也可以手动定义setter(用来设置值),当你给computed属性赋值时,setter会被触发。
(1)setter的基本用法
语法长这样:
computed: { fullName: { get() { return this.firstName + ' ' + this.lastName; }, set(newValue) { // 拆分newValue到firstName和lastName const [first, last] = newValue.split(' '); this.firstName = first; this.lastName = last; } } }
当执行this.fullName = 'John Doe'
时,setter会被调用,firstName
和lastName
会被更新,进而触发fullName
的getter重新计算。
(2)实际用例:表单双向绑定、数据同步
setter在“一个值的变化需要同步修改多个数据源”的场景很有用,
- 表单里的“全名”输入框,需要拆分到
firstName
和lastName
两个字段(用户输入全名,自动拆分存到两个变量)。 - 父子组件传值时,子组件通过computed的setter同步修改父组件传的props(结合v-model语法糖)。
举个表单例子:
<template> <input v-model="fullName" placeholder="输入全名"/> <p>姓:{{ firstName }}</p> <p>名:{{ lastName }}</p> </template> <p><script> export default { data() { return { firstName: '', lastName: '' } }, computed: { fullName: { get() { return this.firstName + ' ' + this.lastName; }, set(val) { const [first, last] = val.split(' '); this.firstName = first || ''; this.lastName = last || ''; } } } } </script>
用户输入“Alice Bob”,firstName
会变成Alice,lastName
变成Bob,模板里实时显示拆分结果。
(3)使用频率:getter远多于setter
日常开发中,getter的使用场景占90%以上,setter只有在需要“反向修改多个数据源”时才用,如果只是简单的“计算值”,用getter足够;复杂的双向同步逻辑,再考虑setter + 合理的依赖管理。
computed和watch怎么选?
很多同学分不清这俩,核心看“目的是‘算值’还是‘执行副作用’”。
(1)computed:专注“推导新值”(多对一)
computed是“基于多个依赖,生成一个新值”,重点在“值的推导”,而且结果会被缓存。
- 根据搜索关键词过滤列表(依赖列表和关键词,生成过滤后的列表)。
- 根据用户选择的商品、数量、优惠,计算最终价格(多依赖→单值)。
(2)watch:专注“响应变化,执行操作”(一对一/多对多)
watch是“监听一个或多个数据的变化,执行副作用(比如异步请求、DOM操作、复杂逻辑)”,重点在“执行操作”,没有缓存(每次变化都触发)。
- 用户登录状态变化时,跳转页面并请求用户信息(监听
isLogin
,执行路由跳转和axios请求)。 - 多个表单字段变化时,实时验证表单(监听
username
、password
等,执行验证逻辑)。 - 监听路由参数变化,重新获取页面数据(比如商品ID变化,重新请求商品详情)。
(3)举个对比例子:搜索功能
需求:用户输入关键词,实时过滤商品列表。
- 用computed:把过滤逻辑放computed里,依赖列表和关键词,返回过滤后的列表,模板渲染过滤后的列表,自动响应数据变化。
- 用watch:监听关键词变化,每次变化时执行过滤函数,把结果存到data里,模板渲染这个data里的过滤后列表。
两种方式都能实现,但computed更简洁(自动缓存、自动响应),watch需要手动维护data和逻辑,所以这种“推导值”的场景优先选computed。
再比如“用户选择城市后,请求该城市的天气数据”:这种“数据变化→执行异步
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。