
引言
Vue 作为前端开发的主流渐进式框架,其响应式系统是实现 "数据驱动视图" 的核心引擎。而在日常开发中,计算属性(computed) 和侦听器(watch/watchEffect) 作为响应式系统的 "左膀右臂",频繁出现在数据处理、状态监控等场景中。
很多开发者初期会混淆二者的用法:什么时候该用 computed?watch 和 watchEffect 到底有啥区别?computed 的缓存机制真的能提升性能吗?本文将从 "原理 + 实战" 双维度,掰开揉碎讲透这两个核心特性,结合真实开发场景和底层逻辑,帮你彻底掌握其用法、区别与最佳实践,让你的 Vue 代码更简洁、高效、易维护。
一、计算属性(computed):带缓存的 "数据加工工厂"
1.1 什么是计算属性?
计算属性是 Vue 提供的一种用于派生数据的特性,它基于依赖的响应式数据进行计算,最终返回一个新值并绑定到视图。
简单来说,当你需要对现有数据进行 "加工处理"(如格式化、筛选、拼接、运算)后再展示时,computed 是最优选择。
基础用法示例:
html
<template>
<div>
<input v-model="firstName" placeholder="名" />
<input v-model="lastName" placeholder="姓" />
<!-- 直接使用计算属性 -->
<p>全名:{{ fullName }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('')
const lastName = ref('')
// 计算属性:依赖firstName和lastName,返回拼接后的全名
const fullName = computed(() => {
// 自动追踪依赖:只有firstName/lastName变化时,才会重新执行
return `${lastName.value}${firstName.value}`
})
</script>
1.2 核心原理:缓存机制到底是怎么工作的?
computed 最核心的优势是缓存机制------ 这也是它和普通方法(method)的本质区别。很多开发者只知道 "computed 有缓存",但不清楚底层逻辑,这里用通俗的语言 + 图解讲透:
缓存机制的 3 个关键步骤:
- 依赖收集:计算属性初始化时,Vue 会执行其内部函数,追踪函数中用到的所有响应式数据(如上面的 firstName、lastName),并将这些数据作为 "依赖项" 记录下来。
- 结果缓存:第一次执行函数后,会将计算结果缓存到 Vue 内部的 "缓存池" 中,后续每次访问 computed 属性时,不会重新执行函数,而是直接返回缓存结果。
- 缓存失效与更新:只有当依赖的响应式数据发生变化时,Vue 才会标记该计算属性 "失效",并在下次访问时重新执行函数、更新缓存,同时触发视图更新。
缓存机制图解:

为什么需要缓存?
- 避免重复计算:如果计算属性的函数逻辑复杂(如循环筛选大数据),频繁执行会消耗性能。
- 减少视图冗余更新:只有依赖变化时才更新,避免无关数据变化导致的无效渲染。
对比 method:为什么不用 method 替代 computed?
html
<!-- 用method实现相同功能 -->
<p>全名:{{ getFullName() }}</p>
<script setup>
const getFullName = () => {
console.log('method执行了')
return `${lastName.value}${firstName.value}`
}
</script>
- 区别:每次组件重新渲染(哪怕是无关数据变化),method 都会重新执行;而 computed 只会在依赖变化时重新计算。
- 结论:需要 "基于响应式数据派生新数据" 时,优先用 computed;需要 "主动触发执行"(如按钮点击事件)时,用 method。
1.3 计算属性的进阶用法:可读写 computed
默认情况下,computed 是只读 的(只能通过依赖变化更新),但在某些场景下,你可能需要手动修改计算属性的值,此时可以传入一个包含get和set的对象:
html
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('')
const lastName = ref('')
// 可读写计算属性
const fullName = computed({
// 读取时执行(和默认用法一致)
get() {
return `${lastName.value}${firstName.value}`
},
// 手动修改时执行:更新依赖数据
set(newValue) {
// 假设输入的newValue格式是"姓 名"
const [last, first] = newValue.split(' ')
lastName.value = last || ''
firstName.value = first || ''
}
})
// 手动修改计算属性(会触发set方法)
fullName.value = '张 三'
</script>
1.4 计算属性的使用场景与踩坑提醒
适用场景:
- 数据格式化:如日期格式化、价格保留两位小数、拼接字符串。
- 数据筛选 / 排序:如从数组中筛选符合条件的元素、对列表排序。
- 依赖多个数据派生新值:如购物车总价(依赖多个商品的价格和数量)。
踩坑提醒:
- 不要在 computed 中修改响应式数据:computed 的核心是 "派生数据",修改数据会导致逻辑混乱,应放在 watch 或事件处理中。
- 避免依赖非响应式数据:如普通变量(let a = 1),Vue 无法追踪其变化,会导致 computed 不更新。
- 复杂计算逻辑可拆分:如果一个 computed 函数过于复杂,可拆分成多个小的 computed,提高可读性和维护性。
二、侦听器:响应式数据变化的 "观察者"
侦听器的核心作用是监听响应式数据的变化 ,并在变化时执行自定义逻辑(如发送请求、修改其他数据、触发回调等)。Vue 提供了两种侦听器:watch和watchEffect,二者功能类似,但使用场景和特性有明显区别。
2.1 watch:精准控制的 "老牌观察者"
watch是 Vue 传统的侦听器,特点是配置灵活、控制精准,可以明确指定要监听的数据源,支持深度监听、立即执行等配置。
基础用法:监听单个响应式数据
html
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// 监听count的变化
watch(count, (newVal, oldVal) => {
console.log(`count从${oldVal}变成了${newVal}`)
// 执行自定义逻辑:如发送统计请求、更新其他数据
})
// 修改count,会触发watch回调
count.value++ // 输出:count从0变成了1
</script>
进阶用法:监听多个数据、深度监听、立即执行
html
<script setup>
import { ref, reactive, watch } from 'vue'
// 1. 监听多个数据
const a = ref(0)
const b = ref(0)
watch([a, b], ([newA, newB], [oldA, oldB]) => {
console.log(`a: ${oldA}→${newA}, b: ${oldB}→${newB}`)
})
// 2. 监听reactive对象(深度监听)
const user = reactive({
name: '张三',
info: { age: 20 }
})
// 监听对象的某个属性(需用函数返回)
watch(
() => user.info.age, // 监听嵌套属性
(newAge, oldAge) => {
console.log(`年龄从${oldAge}变成了${newAge}`)
},
{ deep: true } // 深度监听:默认false,嵌套属性变化需开启
)
// 3. 立即执行(初始时就触发一次回调)
watch(
() => user.name,
(newName) => {
console.log(`当前名字:${newName}`)
},
{ immediate: true } // 初始执行:输出"当前名字:张三"
)
</script>
2.2 watchEffect:自动追踪的 "新一代侦听器"
watchEffect是 Vue 3 新增的侦听器,特点是自动追踪依赖、简洁高效------ 不需要明确指定监听的数据源,它会自动追踪回调函数中用到的所有响应式数据,当这些数据变化时,自动重新执行回调。
基础用法:自动追踪依赖
html
<script setup>
import { ref, watchEffect } from 'vue'
const count = ref(0)
const message = ref('')
// 自动追踪回调中用到的响应式数据(count和message)
const stopWatch = watchEffect(() => {
console.log(`count: ${count.value}, message: ${message.value}`)
})
// 修改count或message,都会触发回调
count.value++ // 输出:count: 1, message:
message.value = 'Hello' // 输出:count: 1, message: Hello
// 手动停止监听(组件卸载时会自动停止)
stopWatch()
</script>
核心特性:
- 自动依赖追踪:回调中用到的响应式数据都会被监听,无需手动指定。
- 立即执行 :默认初始时会执行一次回调,收集依赖(类似 watch 的
immediate: true)。 - 可停止监听:返回一个停止函数,调用后不再监听。
- 清理副作用:支持在回调中返回清理函数,用于清除上一次执行的副作用(如取消请求)。
清理副作用示例(实战常用):
html
<script setup>
import { ref, watchEffect } from 'vue'
import axios from 'axios'
const keyword = ref('')
watchEffect(() => {
// 发送搜索请求(副作用)
const cancelToken = axios.CancelToken.source()
axios.get(`/api/search?keyword=${keyword.value}`, {
cancelToken: cancelToken.token
}).then(res => {
console.log('搜索结果:', res.data)
})
// 清理函数:上一次请求未完成时,取消它
return () => {
cancelToken.cancel('关键词变化,取消上一次请求')
}
})
</script>
- 场景:搜索框输入时,频繁发送请求会导致网络拥堵,清理函数可取消上一次未完成的请求,避免数据错乱。
2.3 watch vs watchEffect:核心区别与选择指南
很多开发者纠结 "什么时候用 watch,什么时候用 watchEffect",这里用表格 + 通俗解释讲清核心区别:
| 对比维度 | watch | watchEffect |
|---|---|---|
| 依赖追踪 | 手动指定监听数据源 | 自动追踪回调中的响应式数据 |
| 初始执行 | 默认不执行(需配置 immediate) | 默认立即执行一次 |
| 监听粒度 | 可监听单个属性、多个属性、嵌套属性 | 只能监听回调中用到的所有依赖 |
| 旧值获取 | 支持获取新旧值(newVal, oldVal) | 不支持直接获取旧值 |
| 配置灵活性 | 支持 deep、immediate 等详细配置 | 配置简单,仅支持 flush 等少数选项 |
| 清理副作用 | 需手动处理 | 支持返回清理函数,自动执行 |
选择指南:
-
用
watch的场景:- 需要明确知道数据的 "旧值" 和 "新值"(如统计数据变化幅度)。
- 只需要监听部分数据(如只监听对象的某个嵌套属性)。
- 不需要初始执行,只在数据变化时触发(如修改密码时验证旧密码)。
-
用
watchEffect的场景:- 不需要旧值,只需在依赖变化时执行副作用(如发送请求、更新 DOM)。
- 依赖较多,手动指定麻烦(如同时依赖多个响应式数据)。
- 需要自动清理副作用(如搜索、防抖、节流场景)。
执行流程对比图解:

三、实战案例:计算属性 + 侦听器协同开发
光说不练假把式,这里用一个 "购物车" 实战案例,演示 computed 和侦听器如何协同工作:
需求:
- 计算购物车总价(基于商品价格和数量,用 computed)。
- 监听总价变化,当总价超过 1000 元时,显示 "满减优惠" 提示(用 watchEffect)。
- 监听商品数量变化,当数量为 0 时,自动移除该商品(用 watch)。
完整代码:
html
<template>
<div class="cart">
<h2>购物车</h2>
<div class="cart-item" v-for="(item, index) in cartList" :key="item.id">
<p>{{ item.name }}</p>
<p>单价:{{ item.price }}元</p>
<button @click="item.count--" :disabled="item.count <= 1">-</button>
<span>{{ item.count }}件</span>
<button @click="item.count++">+</button>
<p>小计:{{ item.count * item.price }}元</p>
</div>
<div class="total">
<h3>总价:{{ totalPrice }}元</h3>
<div class="discount" v-if="showDiscount">🎉 满1000元减200元!</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, watchEffect } from 'vue'
// 购物车数据
const cartList = ref([
{ id: 1, name: 'Vue实战教程', price: 99, count: 1 },
{ id: 2, name: '前端面试宝典', price: 129, count: 2 },
{ id: 3, name: 'TypeScript入门', price: 89, count: 3 }
])
// 1. 计算属性:计算总价
const totalPrice = computed(() => {
return cartList.value.reduce((sum, item) => {
return sum + item.price * item.count
}, 0)
})
// 2. watchEffect:监听总价,显示满减提示
const showDiscount = ref(false)
watchEffect(() => {
showDiscount.value = totalPrice.value >= 1000
})
// 3. watch:监听商品数量,为0时移除商品
cartList.value.forEach((item, index) => {
watch(
() => item.count,
(newCount) => {
if (newCount <= 0) {
cartList.value.splice(index, 1)
}
}
)
})
</script>
<style scoped>
.cart {
width: 500px;
margin: 20px auto;
padding: 20px;
border: 1px solid #eee;
}
.cart-item {
border-bottom: 1px dashed #eee;
padding: 10px 0;
}
.total {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #eee;
}
.discount {
color: red;
font-size: 16px;
margin-top: 10px;
}
</style>
案例解析:
- computed:
totalPrice依赖cartList中所有商品的price和count,自动计算总价,缓存结果,避免重复计算。- watchEffect:自动追踪
totalPrice,当总价达标时显示优惠提示,无需手动配置。- watch:精准监听每个商品的
count,当数量为 0 时执行移除逻辑,需要获取变化后的数量(newCount),适合用 watch。
四、总结与进阶思考
核心总结:
- computed:核心是 "数据派生 + 缓存",适合 "基于现有数据生成新数据" 的场景,优先于 method 使用。
- watch:核心是 "精准监听 + 新旧值对比",适合 "需要明确控制监听逻辑" 的场景,配置灵活。
- watchEffect:核心是 "自动追踪 + 副作用清理",适合 "依赖较多、需要自动执行" 的场景,代码更简洁。
进阶思考:
- 计算属性的缓存是 "惰性的":只有当计算属性被访问时,才会执行计算;如果从未访问,即使依赖变化,也不会计算。
- 侦听器的 "flush" 配置:watch 和 watchEffect 都支持
flush配置(pre/post/sync),控制回调执行时机(如post表示 DOM 更新后执行)。 - 响应式依赖的 "失效":如果侦听器回调中用到的响应式数据被替换(如
ref赋值为新对象),需要确保依赖追踪正确(可使用函数返回的方式)。
面试高频考点:
- 计算属性和 method 的区别?(缓存机制)
- watch 和 watchEffect 的核心区别?(依赖追踪、初始执行、旧值获取)
- 计算属性的缓存机制原理是什么?(依赖收集、缓存存储、失效更新)
- 如何监听 reactive 对象的嵌套属性?(watch 用函数返回,或开启 deep;watchEffect 自动追踪)
互动交流
如果你在使用 computed 或侦听器时遇到过有趣的场景,或者有其他疑问(如 "复杂场景下如何优化侦听器性能"),欢迎在评论区留言讨论!如果本文对你有帮助,别忘了点赞 + 收藏,关注我,后续会持续更新 Vue 核心原理与实战技巧~