一、写在前面
学完 ref 和 reactive 之后,很多新手会进入下一个典型阶段:
我已经会定义响应式数据了,也会改数据了,页面也能跟着更新。
但是很快又会碰到新的问题。
比如:
-
我有姓名和年龄,能不能自动拼成一段介绍?
-
我有商品单价和数量,能不能自动得到总价?
-
输入框内容变化时,我想同步打印日志怎么办?
-
某个状态变化后,我想发请求、保存本地数据、做一些副作用处理怎么办?
这时候你就会接触到 Vue3 响应式系统里另外三个非常重要的能力:
-
computed -
watch -
watchEffect
很多新手一开始看到这三个东西,会本能地觉得有点乱。
因为它们都和"数据变化"有关,但又不是一回事。
所以这一篇文章的目标,就是把它们之间的边界讲清楚。
你可以先记住一个非常核心的思路:
computed更偏"算出一个值",watch更偏"监听变化后执行动作",watchEffect更偏"自动收集依赖并立即运行"。
后面我们就把这句话彻底拆开讲明白。
二、为什么会需要 computed 和 watch?
先从最本质的问题开始。
你已经知道,Vue3 的核心是响应式。
也就是:
-
数据变了
-
页面会自动更新
但真实项目里,不是所有内容都直接来自"原始数据"。
很多时候,页面上显示的东西其实是"算出来的结果"。
比如:
-
姓名 + 年龄 = 一段介绍文字
-
商品价格 × 数量 = 总价
-
列表过滤后 = 新列表
-
是否登录 = 显示不同状态文案
还有一些场景,数据变化后你并不是想"显示一个新值",而是想"顺便做一件事"。
比如:
-
输入框变化后发请求
-
状态变化后保存到本地
-
某个 id 变化后重新获取详情
-
页面参数变化后打印日志
所以你会发现,围绕"数据变化",其实至少有两类需求:
第一类:我想得到一个新的结果值
这种通常更适合 computed
第二类:我想在数据变化后执行额外动作
这种通常更适合 watch
而 watchEffect,则是另一种更自动化的监听方式。
三、computed 到底是什么?
先给一个最直接的定义:
computed是计算属性,用来根据已有响应式数据,计算出一个新的值。
注意这里的关键词是:
-
根据已有数据
-
计算出新值
也就是说,computed 更像是:
由别的数据推导出来的结果。
它不是拿来随便执行一段逻辑的,而是更偏向"得出一个可直接使用的值"。
四、一个最简单的 computed 例子
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const fullName = computed(() => {
return firstName.value + lastName.value
})
</script>
<template>
<div>
<p>姓:{{ firstName }}</p>
<p>名:{{ lastName }}</p>
<p>姓名:{{ fullName }}</p>
</div>
</template>
这里发生了什么?
-
firstName是原始数据 -
lastName是原始数据 -
fullName不是手动写死的,而是算出来的
也就是说:
fullName是一个"派生值"。
只要 firstName 或 lastName 变化,fullName 就会自动跟着更新。
这就是计算属性最经典的使用场景。
五、为什么不用普通函数,而要用 computed?
这是非常关键的问题。
很多新手看到上面的代码,第一反应会是:
"这不就是个函数吗?我直接写个函数返回拼接结果不就行了?"
比如:
<script setup>
import { ref } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const getFullName = () => {
return firstName.value + lastName.value
}
</script>
<template>
<p>{{ getFullName() }}</p>
</template>
这当然也能工作。
但 computed 和普通函数,核心区别不在"能不能得到结果",而在于:
computed会基于依赖做缓存,而普通函数每次渲染都会重新执行。
六、什么叫"缓存"?
可以先这样理解:
如果 computed 依赖的数据没有变化,那么它不会反复重新计算。
它会直接复用上一次的结果。
例如:
const fullName = computed(() => {
console.log('我重新计算了')
return firstName.value + lastName.value
})
如果 firstName 和 lastName 没变,那么模板多次读取 fullName 时,并不会一直重新跑这段逻辑。
这就是 computed 的价值之一:
它适合做"基于响应式数据推导出的值",而且有缓存。
所以你可以这样理解:
-
普通函数:调用一次算一次
-
computed:依赖没变,就尽量复用已有结果
对于复杂计算来说,这会更高效,也更符合 Vue 的响应式思路。
七、computed 最适合哪些场景?
你可以把计算属性理解成"展示层的派生结果"。
最常见的场景包括:
1. 拼接展示内容
const fullName = computed(() => {
return firstName.value + lastName.value
})
2. 计算总价
const totalPrice = computed(() => {
return price.value * count.value
})
3. 计算过滤后的列表
const doneList = computed(() => {
return list.value.filter(item => item.done)
})
4. 根据状态返回文案
const statusText = computed(() => {
return isLogin.value ? '已登录' : '未登录'
})
你会发现它们有一个共同点:
都不是"额外执行动作",而是在"产出一个新值"。
这就是判断能不能用 computed 的关键标准。
八、computed 的写法怎么理解?
最常见写法是:
const 变量名 = computed(() => {
return 某个计算结果
})
比如:
const doubleCount = computed(() => {
return count.value * 2
})
它的语义其实非常直白:
-
我定义了一个叫
doubleCount的计算属性 -
它的值由
count推导而来 -
count变了,它就重新计算
你可以把它理解成"响应式版本的自动计算结果"。
九、watch 又是什么?
如果说 computed 是"根据已有数据算出一个新值",
那么 watch 更像是:
我专门盯着某个数据,一旦它变了,就执行一段逻辑。
注意关键词是:
-
盯着某个数据
-
变了之后执行逻辑
这和 computed 的方向明显不同。
computed 更像"我要一个结果"。
watch 更像"我要在变化发生后做事"。
十、一个最简单的 watch 例子
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newValue, oldValue) => {
console.log('新值:', newValue)
console.log('旧值:', oldValue)
})
</script>
<template>
<div>
<p>{{ count }}</p>
<button @click="count++">加一</button>
</div>
</template>
这里的意思是:
-
watch在监听count -
只要
count变化 -
回调函数就会执行
-
并且你还能拿到新值和旧值
所以 watch 的基本思路是:
监听谁,谁变了,我就触发一个回调。
十一、watch 更适合做什么?
这一点非常关键。
watch 一般不拿来"专门算一个值",
而更适合做"副作用逻辑"。
什么叫副作用?
你可以先简单理解成:
不是为了直接返回一个值,而是为了在变化后额外执行某些动作。
例如:
1. 打印日志
watch(count, (newValue) => {
console.log('count 变成了:', newValue)
})
2. 保存到本地存储
watch(username, (newValue) => {
localStorage.setItem('username', newValue)
})
3. 参数变化后发请求
watch(userId, async (newId) => {
// 根据新 id 获取用户详情
})
4. 表单变化后做校验
watch(email, (newValue) => {
// 做邮箱格式检查
})
你会发现,这些都不是在"生成一个展示值",
而是在"变化发生后执行逻辑"。
所以:
-
要"算值",优先想
computed -
要"监听变化做事",优先想
watch
十二、watch 的回调参数怎么理解?
看这个例子:
watch(count, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
这里:
-
newValue是变化后的新值 -
oldValue是变化前的旧值
例如 count 从 1 变成 2,那么:
-
newValue = 2 -
oldValue = 1
这在调试和业务处理中很有用,因为你不仅知道"变了",还知道"怎么变的"。
十三、watch 能监听多个数据吗?
可以。
例如:
<script setup>
import { ref, watch } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log('新值:', newFirst, newLast)
console.log('旧值:', oldFirst, oldLast)
})
</script>
这表示:
-
同时监听
firstName和lastName -
只要它们其中一个变了,就会触发回调
这在多条件联动时很有用。
十四、watch 监听对象时要注意什么?
如果你监听的是对象,会稍微复杂一点。
例如:
const user = reactive({
name: '张三',
age: 20
})
如果你直接写:
watch(user, (newValue, oldValue) => {
console.log('user 变了')
})
这是可以工作的,因为 reactive 对象本身就是响应式对象。
但如果你监听的是对象里的某一个属性,更推荐这样写:
watch(() => user.name, (newValue, oldValue) => {
console.log('姓名变化了')
})
这里的写法很重要:
当你想监听某个属性时,通常要传一个函数过去。
也就是:
() => user.name
这样 Vue 才知道你到底在监听什么。
十五、watchEffect 又是什么?
现在到了很多新手更容易混乱的地方。
先给定义:
watchEffect会立即执行一次回调,并自动收集回调中用到的响应式依赖;这些依赖变化后,它会重新执行。
这个定义看起来有点绕,我们拆开讲。
1. 它会先立即执行一次
例如:
<script setup>
import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => {
console.log('当前 count 是:', count.value)
})
</script>
这段代码一运行,回调会立刻执行一次。
哪怕你还没有点击任何按钮。
2. 它会自动收集依赖
在这个例子里,回调里用到了:
count.value
所以 Vue 会自动知道:
- 这个
watchEffect依赖了count
以后只要 count 变化,回调就会再次执行。
3. 它和 watch 最大的区别是什么?
你可以先这样记:
watch
你要明确告诉它"监听谁"
watch(count, () => {
// ...
})
watchEffect
你不用手动指定"监听谁",它会根据回调里实际用到的响应式数据自动收集依赖
watchEffect(() => {
console.log(count.value)
})
所以:
watch是显式监听,watchEffect是自动收集依赖。
十六、watchEffect 更适合什么场景?
它比较适合这种情况:
我不太想手动列出监听源,我只是想写一段依赖响应式数据的逻辑,让它自动跟着跑。
比如:
1. 自动打印某些状态
watchEffect(() => {
console.log('当前用户名:', username.value)
console.log('当前年龄:', age.value)
})
这里它会自动依赖 username 和 age。
2. 初始化和后续变化都要执行同一段逻辑
比如:
-
页面一进来就执行一次
-
后面依赖变了也继续执行
这种"立即执行 + 自动依赖追踪"的模式,很适合 watchEffect
十七、computed、watch、watchEffect 三者到底怎么区分?
这是这一篇最关键的总结部分。
我们直接从使用意图来区分。
1. computed
当你想:
根据已有响应式数据,得到一个新的结果值
就优先考虑 computed
比如:
-
全名
-
总价
-
已完成任务数量
-
过滤后的列表
-
状态文案
一句话理解:
它是"算结果"的。
2. watch
当你想:
明确监听某个或某些数据,一旦变化就执行动作
就优先考虑 watch
比如:
-
数据变化后发请求
-
状态变化后存本地
-
监听路由参数变化
-
表单项变化后校验
一句话理解:
它是"盯变化做事"的。
3. watchEffect
当你想:
写一段依赖响应式数据的逻辑,让它先执行一次,以后依赖变了自动再执行
就可以考虑 watchEffect
一句话理解:
它是"自动依赖收集 + 立即执行"的监听方式。
十八、一个综合例子:三者放在一起看
下面给你一个综合示例,把三者放到同一个组件里理解。
<script setup>
import { ref, computed, watch, watchEffect } from 'vue'
const price = ref(100)
const count = ref(2)
const totalPrice = computed(() => {
return price.value * count.value
})
watch(count, (newValue, oldValue) => {
console.log(`数量从 ${oldValue} 变成了 ${newValue}`)
})
watchEffect(() => {
console.log(`当前单价:${price.value},当前数量:${count.value}`)
})
</script>
<template>
<div>
<p>单价:{{ price }}</p>
<p>数量:{{ count }}</p>
<p>总价:{{ totalPrice }}</p>
<button @click="count++">数量加一</button>
</div>
</template>
这里三者的分工非常清晰:
-
totalPrice:负责算结果,用computed -
watch(count, ...):明确监听数量变化,用watch -
watchEffect(...):自动读取当前依赖,并立即执行,用watchEffect
这个例子如果你看顺了,这一篇的主线就差不多通了。
十九、新手最常见的几个误区
这一部分非常重要,因为很多人就是在这里开始"似懂非懂"。
1. 把 computed 当成"普通函数替代品"
computed 不是为了单纯省几行函数代码,
它的重点是:
-
它表示一个"派生值"
-
它有响应式依赖
-
它有缓存
所以不要把它简单理解成"另一种函数"。
2. 用 watch 去计算展示值
例如你为了得到总价,写成:
watch([price, count], () => {
total.value = price.value * count.value
})
虽然不是绝对不能做,但这个场景更自然的写法通常是 computed。
因为总价本质上就是"由别的数据推导出的结果"。
所以:
能直接算出结果的,优先想
computed。
3. 看到监听就无脑用 watchEffect
watchEffect 虽然方便,但不是所有监听都该用它。
如果你想精准监听某个明确数据,并且想区分新旧值,
通常 watch 更合适。
因为 watchEffect:
-
自动收集依赖
-
没有显式监听源那么直观
-
某些情况下不如
watch可控
所以前期不要把它当万能工具。
4. 分不清"值"和"动作"
你可以用一个特别实用的判断法:
如果你需要的是"一个值"
比如总价、全名、过滤结果
优先考虑 computed
如果你需要的是"执行一个动作"
比如请求、日志、存储、校验
优先考虑 watch / watchEffect
这个判断法特别有用。
二十、这一篇学完后,你应该达到什么程度?
如果你把这篇真正理解了,至少应该做到:
-
知道
computed是计算属性 -
知道
computed适合产出派生值 -
知道它和普通函数的重要区别是缓存
-
知道
watch是显式监听数据变化 -
知道
watch更适合做副作用逻辑 -
知道
watchEffect会立即执行并自动收集依赖 -
能大致判断什么时候用
computed,什么时候用watch -
看到这三者的代码时,不再觉得它们像一团东西
只要这一层真正清楚了,你对 Vue3 响应式系统的掌握就已经从"会改数据"进入到"会组织响应式逻辑"了。
二十一、总结
这一篇文章,我们把 Vue3 响应式系统里的三个核心能力串起来讲清楚了:
-
computed:根据已有响应式数据,计算出新的结果值 -
watch:明确监听某个数据的变化,并在变化后执行逻辑 -
watchEffect:自动收集依赖,立即执行,并在依赖变化后重新执行
最重要的不是死记定义,而是建立下面这个判断意识:
-
要结果,用
computed -
要监听变化做事,用
watch -
要自动依赖收集并立即执行,可以考虑
watchEffect
如果说上一篇让你理解了 Vue3 的"响应式状态"是怎么定义的,
那么这一篇,就是让你开始真正掌握:
响应式状态变化之后,值怎么推导、逻辑怎么联动。
这会直接影响你后面写表单、写组件通信、写项目状态管理时的思路。