本文基于 Vue3 主流的
<script setup>语法糖编写,通俗易懂、知识点全覆盖、区分清晰,从概念、用法、特性、区别、使用场景到实战案例,带你彻底吃透 computed 和 watch,看完再也不会混淆二者用法 ✨
一、前言
在 Vue3 中,computed(计算属性) 和 watch(监听属性) 是处理响应式数据的两大核心 API,也是 Vue 开发中高频使用的基础知识点。二者都能监听响应式数据的变化,但设计初衷、使用场景、特性完全不同。
很多初学者容易混淆它们的用法,比如:什么时候用计算属性?什么时候用监听属性?为什么明明能用 computed 实现的功能,用 watch 会显得臃肿?
核心结论先记住:
- 计算属性 :衍生新数据,用声明式的方式处理数据,做「数据加工」。
- 监听属性 :监听数据变化,执行自定义的业务逻辑,做「事件处理」。
前置基础(必看)
在 Vue3 的 <script setup> 语法中,所有的组合式 API 都需要按需导入 后使用,这是 Vue3 的性能优化方案(按需引入,减少打包体积),computed 和 watch 也不例外,基础依赖导入如下:
javascript
运行
import { ref, reactive, computed, watch, watchEffect } from 'vue'
补充:
computed/watch都是基于ref和reactive创建的响应式数据工作,这是学习本文的前置基础。
二、计算属性 computed 详解
2.1 核心概念
计算属性,本质是基于现有响应式数据,经过「计算 / 加工 / 处理」后,返回一个新的响应式数据 。简单理解:由其他数据派生出来的新数据,就是计算属性。
2.2 核心特性(重中之重,面试高频)
✅ 特性 1:【响应式依赖】自动追踪依赖源
计算属性的返回值,依赖于声明的「源响应式数据」,当源数据发生变化时,计算属性的值会自动更新,页面也会同步刷新,完全不用手动干预。
✅ 特性 2:【缓存机制】性能优化核心(最核心)
这是计算属性最核心、最实用的特性,也是和「方法」最大的区别:
- 只要计算属性依赖的源数据没有发生变化,无论多少次访问这个计算属性,都会直接返回「上一次计算的缓存结果」,不会重复执行计算逻辑。
- 只有当依赖的源数据发生改变,计算属性才会重新执行内部的计算逻辑,生成新的值并缓存。
✅ 特性 3:【默认只读】简洁安全
计算属性默认是只读的,只能根据源数据计算出新值,不能直接修改计算属性本身的值,这种设计可以避免修改派生数据导致的逻辑混乱,保证数据流向的清晰。
✅ 特性 4:【声明式语法】更优雅
计算属性用「声明式」的写法处理数据逻辑,相比方法调用,代码更简洁、语义化更强,Vue 模板中使用时,和普通响应式数据的用法一致,不需要加括号调用。
2.3 两种使用写法
✔ 写法 1:只读型计算属性(99% 的业务场景都用这个)
语法格式:传入一个回调函数,回调函数的返回值就是计算属性的值,默认只能读取,不能修改。
vue
<template>
<div>
<p>num1:{{ num1 }}</p>
<p>num2:{{ num2 }}</p>
<p>求和结果(计算属性):{{ sum }}</p>
<p>多次访问计算属性:{{ sum }} {{ sum }} {{ sum }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 源响应式数据
const num1 = ref(10)
const num2 = ref(20)
// 计算属性:求和(依赖 num1 和 num2)
const sum = computed(() => {
console.log('计算属性的逻辑执行了')
return num1.value + num2.value
})
</script>
运行结果:
- 页面初始化时,控制台打印一次
计算属性的逻辑执行了,页面中多次访问sum都不会重复打印。 - 当修改
num1.value或num2.value时,控制台会再次打印,sum的值也会自动更新。
✔ 写法 2:可读可写型计算属性(特殊场景使用)
默认的计算属性是只读的,但在一些特殊业务场景中,需要手动修改计算属性的值,此时可以给 computed 传入一个配置对象,对象中包含两个方法:
get():取值时执行的函数,逻辑和只读型计算属性一致,返回计算后的新值。set(val):赋值时执行的函数,参数val是给计算属性赋的新值,在这个方法中可以手动修改「源响应式数据」。
核心原则:就算开启了可写,也不能直接修改计算属性 ,而是在
set中修改「源数据」,由源数据再派生出计算属性的值。
vue
<template>
<div>
<p>原始值:{{ num }}</p>
<p>计算属性(翻倍):{{ doubleNum }}</p>
<button @click="changeNum">修改计算属性的值为 50</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const num = ref(10)
// 可读写的计算属性
const doubleNum = computed({
// 读取时执行
get() {
return num.value * 2
},
// 修改时执行,val 是新赋值的值
set(val) {
num.value = val / 2
}
})
// 手动修改计算属性的值
const changeNum = () => {
doubleNum.value = 50
}
</script>
运行结果 :点击按钮后,doubleNum 被赋值为 50,触发 set 方法,num 的值被修改为 25,随后 get 方法执行,doubleNum 的值变成 50,页面同步刷新。
2.4 计算属性 vs 方法,为什么不用方法?
很多人会有疑问:既然计算属性能实现的功能,用方法也能实现(比如求和、格式化),为什么还要用计算属性?答案就是:缓存机制!
举个例子,用方法实现上面的求和功能:
javascript
运行
// 方法写法
const getSum = () => {
console.log('方法执行了')
return num1.value + num2.value
}
模板中调用:<p>{``{ getSum() }}</p>
核心区别:
- 方法 :每次页面渲染(包括组件重新渲染、其他数据变化),或者每次调用方法,都会重新执行方法的逻辑,没有缓存,性能开销大。
- 计算属性:只有依赖的源数据变化,才会重新计算,否则永远走缓存,性能更高。
2.5 适用场景(记住一句话:无脑用 computed 的场景)
当需要对现有响应式数据做「格式化、运算、拼接、筛选、转换」,最终得到一个新的数据时,优先使用计算属性!
这是 computed 的黄金使用法则,常见业务场景举例:
- 数据运算:数字的加减乘除、取余、求和、平均值等。
- 数据格式化:时间戳转日期格式、数字转金额格式(如
1000 → ¥1000.00)、手机号脱敏等。 - 数据拼接:姓 + 名 = 完整姓名、省市区拼接成完整地址等。
- 列表筛选 / 过滤:根据条件过滤数组(如筛选出列表中的偶数、筛选出状态为已完成的任务)。
- 状态判断:根据多个变量判断一个状态(如
isDisabled = computed(() => !name.value || !phone.value))。
2.6 经典实战案例
案例:列表筛选(高频业务场景)
vue
<template>
<div>
<p>原始数组:{{ list }}</p>
<p>偶数列表(计算属性):{{ evenList }}</p>
<p>大于5的数字列表(计算属性):{{ greaterFiveList }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const list = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
// 筛选出偶数列表
const evenList = computed(() => list.value.filter(item => item % 2 === 0))
// 筛选出大于5的数字列表
const greaterFiveList = computed(() => list.value.filter(item => item > 5))
</script>
三、监听属性 watch 详解
3.1 核心概念
监听属性,本质是监听一个或多个响应式数据的变化,当数据发生改变时,执行一段自定义的「业务逻辑代码」 。简单理解:你盯着某个数据,这个数据一旦变了,你就去做某件事。
3.2 核心特性
✅ 特性 1:【监听数据变化】被动触发
watch 是「被动触发」的,只有当被监听的数据发生变化时,才会执行回调函数中的业务逻辑,没有数据变化则不会执行。
✅ 特性 2:【无返回值】专注业务逻辑
watch 没有返回值,它的核心作用不是生成新数据,而是处理业务逻辑,比如:发起网络请求、修改其他数据、操作 DOM、提示弹窗、路由跳转等。
✅ 特性 3:【精准监听】可指定监听目标
watch 可以精准监听单个数据、多个数据、对象的某个属性,监听目标明确,不会像计算属性那样自动追踪依赖,灵活性更高。
✅ 特性 4:【支持配置项】功能更强大
watch 提供了丰富的配置项,比如:立即执行、深度监听、取消监听等,可以满足各种复杂的业务场景。
3.3 基础使用写法(全场景覆盖,必学)
watch 的基础语法:接收三个参数,返回一个「停止监听」的函数
javascript
运行
const stopWatch = watch(监听目标, 回调函数, 配置项)
✔ 写法 1:监听单个 ref 数据(最基础,最常用)
监听由 ref 声明的单个响应式数据,数据变化时,执行回调函数。回调函数的两个参数:
newVal:数据变化后的「新值」oldVal:数据变化前的「旧值」
vue
<template>
<div>
<p>当前数字:{{ num }}</p>
<button @click="num++">点击加1</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const num = ref(0)
// 监听单个ref数据
watch(num, (newVal, oldVal) => {
console.log('num发生了变化', '新值:', newVal, '旧值:', oldVal)
// 这里可以写任意业务逻辑:比如请求接口、修改其他数据、弹窗提示等
if (newVal >= 5) {
alert('数字已经大于等于5了!')
}
})
</script>
✔ 写法 2:监听多个响应式数据
如果需要同时监听多个数据的变化,只需要把「监听目标」写成一个数组即可,数组中的每一项都是要监听的响应式数据。回调函数的参数也是数组,顺序和监听目标一致,分别对应每个数据的新值和旧值。
javascript
运行
import { ref, watch } from 'vue'
const num1 = ref(0)
const num2 = ref(0)
// 监听多个数据
watch([num1, num2], ([newNum1, newNum2], [oldNum1, oldNum2]) => {
console.log('num1或num2发生了变化')
console.log('num1:', newNum1, oldNum1)
console.log('num2:', newNum2, oldNum2)
})
✔ 写法 3:监听 reactive 声明的对象 / 数组(深度监听)
对于由 reactive 声明的复杂数据类型(对象 / 数组) ,watch 会默认开启「深度监听」,无需手动配置,只要对象 / 数组内部的属性发生变化,就会触发回调。
注意:监听 reactive 对象时,回调函数的
oldVal会和newVal指向同一个引用,无法获取真正的旧值,这是 Vue3 的已知特性。
vue
<template>
<div>
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
<button @click="user.age++">修改年龄</button>
<button @click="user.name = '李四'">修改姓名</button>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
const user = reactive({
name: '张三',
age: 20
})
// 监听reactive声明的对象
watch(user, (newVal, oldVal) => {
console.log('user对象发生了变化', newVal)
// 业务逻辑:比如保存用户信息到本地存储
localStorage.setItem('user', JSON.stringify(newVal))
})
</script>
✔ 写法 4:监听对象的单个属性(精准监听)
很多时候,我们不需要监听整个对象,只需要监听对象的某个具体属性 ,此时可以把「监听目标」写成一个回调函数,返回要监听的属性值即可。
javascript
运行
import { reactive, watch } from 'vue'
const user = reactive({
name: '张三',
age: 20
})
// 只监听user对象的age属性
watch(() => user.age, (newVal, oldVal) => {
console.log('年龄发生了变化', newVal, oldVal)
})
3.4 常用配置项(3 个高频配置,必掌握)
watch 的第三个参数是一个配置对象,用来扩展监听的功能,常用的配置项只有 3 个,简单好记,覆盖 99% 的场景:
✅ 配置 1:immediate - 立即执行
默认情况下,watch 是「惰性执行」的:页面初始化时,不会执行回调函数,只有当监听的数据发生变化时才会执行。如果需要让 watch 在页面初始化时就立即执行一次 ,可以配置 immediate: true,这个配置在「页面加载时发起请求」的场景中非常常用。
javascript
运行
watch(num, (newVal) => {
console.log('立即执行+数据变化时执行', newVal)
}, { immediate: true })
✅ 配置 2:deep - 深度监听
当监听的是「ref 声明的复杂数据类型(对象 / 数组)」时,watch 默认不会监听内部属性的变化,此时需要手动配置 deep: true,开启深度监听。
补充:监听
reactive声明的对象时,默认开启深度监听,无需手动配置。
javascript
运行
import { ref, watch } from 'vue'
const user = ref({
name: '张三',
age: 20
})
// 监听ref声明的对象,需要开启深度监听
watch(user, (newVal) => {
console.log('用户信息变化了', newVal)
}, { deep: true })
✅ 配置 3:取消监听
watch 会返回一个「停止监听」的函数,调用这个函数后,watch 将不再监听数据的变化,适合在「组件卸载」或「满足某个条件」时取消监听,避免内存泄漏。
javascript
运行
import { ref, watch } from 'vue'
const num = ref(0)
// 接收停止监听的函数
const stopWatch = watch(num, (newVal) => {
console.log('num变化了', newVal)
if (newVal >= 5) {
// 满足条件,取消监听
stopWatch()
}
})
3.5 适用场景(记住一句话:无脑用 watch 的场景)
当需要监听某个数据的变化,去执行一段「自定义的业务逻辑」时,优先使用 watch!
这是 watch 的黄金使用法则,watch 的核心是「处理逻辑」,不是「处理数据」,常见业务场景举例:
- 数据变化时,发起网络请求(比如:搜索框输入内容变化,发起搜索请求)。
- 数据变化时,修改其他相关数据(比如:用户选择的城市变化,更新对应的地区列表)。
- 数据变化时,执行弹窗提示、路由跳转、本地存储等操作。
- 监听路由参数的变化,做页面的刷新或数据的重新加载。
- 满足某个条件时,执行特定的业务逻辑(比如:数字大于 5 时弹窗提示)。
四、computed 和 watch 的核心区别(重中之重,必背)
这是本文的核心重点,也是面试中必考的高频问题 ,更是区分新手和老手的关键,一定要彻底理解并记住,二者的区别主要体现在以下 6 个维度,从核心到细节,一目了然:
✅ 核心区别 1:【设计初衷不同】------ 本质区别
- computed :为了派生新数据 ,是「数据层面」的处理,它的产出是新的数据。
- watch :为了监听数据变化 ,是「行为层面」的处理,它的产出是执行业务逻辑。
✅ 核心区别 2:【是否有返回值】
- computed :必须有返回值,回调函数的返回值就是计算属性的值,没有返回值则无意义。
- watch :没有返回值,回调函数中执行的是业务逻辑,不需要返回任何内容。
✅ 核心区别 3:【是否依赖缓存】
- computed :有缓存机制,依赖源不变则复用缓存,性能更高。
- watch :无缓存机制,只要监听的数据发生变化,就会执行回调函数,每次都是全新执行。
✅ 核心区别 4:【数据流向不同】
- computed :单向依赖,只能由「源数据」派生新数据,默认不能反向修改,数据流向清晰。
- watch :无固定流向,监听数据变化后,可以修改任意其他数据,灵活性更高。
✅ 核心区别 5:【触发方式不同】
- computed :自动触发,当依赖的源数据变化时,计算属性会自动更新,无需手动调用。
- watch :被动触发 ,只有监听的数据发生变化时,才会执行回调,也可以通过配置
immediate主动触发。
✅ 核心区别 6:【使用场景不同】
- computed :适合数据加工,凡是需要对现有数据做格式化、运算、筛选、拼接的场景,都用 computed。
- watch :适合业务逻辑处理,凡是需要监听数据变化做请求、跳转、弹窗、修改其他数据的场景,都用 watch。
一句话总结区别(终极版)
能用 computed 实现的需求,一定不要用 watch;用 watch 实现的需求,computed 一定做不到。
五、补充:监听的语法糖 watchEffect
在 Vue3 中,还有一个和 watch 功能类似的 API ------ watchEffect,它是 watch 的「语法糖」,也叫「立即执行的监听」,这里做简单补充,方便大家在项目中灵活选择:
核心特点
watchEffect不需要指定监听目标,它会自动追踪回调函数中使用的所有响应式数据,只要其中任意一个数据变化,就会执行回调。watchEffect是「立即执行」的,页面初始化时会执行一次,无需配置immediate: true。- 写法更简洁,适合「多个数据变化都要执行相同逻辑」的场景。
简单示例
javascript
运行
import { ref, watchEffect } from 'vue'
const num1 = ref(0)
const num2 = ref(0)
// 自动监听回调中用到的num1和num2
watchEffect(() => {
console.log('num1或num2变化了', num1.value, num2.value)
})
watch vs watchEffect 怎么选?
- 知道要「监听哪个具体数据」→ 用
watch,精准监听,性能更高。 - 不知道要「监听哪些数据」,只知道「要执行什么逻辑」→ 用
watchEffect,写法简洁。
六、总结
核心知识点回顾
- 计算属性
computed:派生新数据,有缓存,声明式,只读优先,适合做数据加工。 - 监听属性
watch:监听数据变化,无缓存,命令式,执行逻辑,适合做业务处理。 - 二者都是 Vue3 处理响应式数据的核心 API,相辅相成,没有优劣之分,只有适用场景不同。
最佳实践(新手避坑指南)
- 不要滥用 watch:很多新手会用 watch 处理所有数据变化,比如用 watch 做求和、格式化,这是错误的,不仅代码臃肿,还浪费性能,这类需求一定要用 computed。
- 不要依赖 computed 做业务逻辑:计算属性的核心是返回新数据,不要在 computed 中执行请求、弹窗、修改其他数据等业务逻辑,这类需求一定要用 watch。
- 优先使用 computed :如果一个需求既可以用 computed 也可以用 watch 实现,优先用 computed,因为它更简洁、性能更高。
最后一句口诀(记牢终身受用)
数据变,出新数 → computed;数据变,做事情 → watch。
希望这篇文章能帮助你彻底理解 Vue3 的 computed 和 watch,从此在开发中不再混淆,游刃有余 ✨。