Vue3 计算与侦听
有了 ref 和 reactive,数据会响应式更新了。但光会更新还不够,数据之间往往还有两种”联动”需求:
- 根据已有数据算出新数据:购物车总价、筛选后的列表、折扣价。
- 数据变化时去做点事:保存草稿、发起搜索请求、更新页面标题——这类”连锁反应”。
在Vue3里,computed 管”算”,watch 管”指定数据一变就做事”,watchEffect 管”不指定,用到的数据变了就自动重跑”。
先把三个 API 的基本形态摆出来,混个眼熟,正文再逐个展开:
import { computed, watch, watchEffect } from 'vue'
// computed:根据已有数据算出新值,返回一个只读 ref
computed(() => price.value * count.value)
// watch:点名监听某个数据,一变就执行回调
watch(keyword, (newVal, oldVal) => { /* 变化后的处理 */ })
// watchEffect:不点名,回调里用到哪个数据,就自动听哪个
watchEffect(() => { /* 同步副作用 */ })
三者都是”丢一个函数进去”,区别全在依赖上:computed 靠读数据自动收集依赖(还能缓存结果);watch 靠手动点名;watchEffect 不点名,用了谁就听谁。
computed
基本用法
它接收一个”纯计算函数”,返回一个只读的 ref。
特点是自动追踪依赖、结果会被缓存,依赖没变,访问多少次都直接用缓存,不会重复计算。
<script setup>
import { ref, computed } from 'vue'
const price = ref(89)
const count = ref(2)
// 总价:依赖 price 和 count,任何一个变了才会重新计算
const total = computed(() => price.value * count.value)
</script>
<template>
<p>单价 {{ price }},数量 {{ count }}</p>
<p>总价:{{ total }}</p>
</template>
几点细节:
total也是ref,脚本中用total.value读,模板直接写total。- computed 内做的是同步计算,不要在里面发请求、改别的数据(那是副作用,想办法留给
watch)。
为什么需要 computed
模板里直接写函数调用行不行?为什么还要 computed?
试试用函数:定义一个 total 函数,模板里 {{ total() }} 调用,结果一样能显示:
const total = () => price.value * count.value
<p>总价:{{ total() }}</p>
但每次模板重新渲染都会重新调用这个函数,没有缓存;computed 在依赖不变时不重算,列表大、计算量大时,差距明显。
可写 computed
上面的 total 是只读的——它由别的数据派生,你只能读它,不能改它。可现实中经常要”既能展示、又能修改”的派生值。
举个最常见的例子:有一个 first(名)和一个 last(姓)两个字段,但输入框里想只显示一个合并的”全名”。先用 computed 拼出来:
<script setup>
import { ref, computed } from 'vue'
const first = ref('张')
const last = ref('三')
// 合并成全名 —— 只能读,不能写
const fullName = computed(() => first.value + ' ' + last.value)
</script>
<template>
<input v-model="fullName" placeholder="编辑全名" />
</template>
模板里 v-model="fullName" 要做两件事:
- 显示时读
fullName - 输入时把新值写回
fullName
可上面的 computed 只有”读”(get)没有”写”的入口,所以用户一打字写回就报错:Write operation failed: computed value is readonly。
要能回填数据,就得给 computed 配上一个 set,把输入的新全名拆回 first 和 last,把”只读”升级成”可读写”:
const fullName = computed({
// 读:怎么把 first + last 拼成全名 —— 用于展示
get: () => first.value + ' ' + last.value,
// 写:用户改了全名,怎么拆回去更新 first 和 last —— 由你决定
set: (v) => {
const parts = v.split(' ')
first.value = parts[0]
last.value = parts[1]
}
})
流程是:显示时走 get 拼接全名,用户输入新全名 → set 收到新值拆开写回 first、last → 底层数据变了,get 又自动重新拼接,展示同步更新。
这样 first / last 依然是唯一的数据源,fullName 只是它们的一个”代理壳”,不会出现两处数据对不上的情况。
watch
watch 用来”监听某个数据,变了就去执行一段逻辑”。
const user = ref({ name: '小明', age: 18 })
watch(user, (newVal) => {
console.log('用户信息变化', newVal)
}, { deep: true, immediate: true })
基本语法:watch(监听来源, 回调, 选项?),三个参数各管一件事:
- 监听来源:监听谁。单个
ref、多个ref组成的数组,或一个”返回值的函数”(getter,如() => user.value.age)。 - 回调:来源一变就执行。回调会自动收到两个参数——新值
newVal和 旧值oldVal,你可以直接用,也可以不接。监听多个来源时收到的是数组,如([newK, newC]) => ...。 - 选项:
immediate、deep等配置放第三个参数,见下面的表格。
常用配置项:
| 选项 | 作用 |
|---|---|
immediate: true |
监听建立时立即执行一次回调(常用于”初始也加载数据”) |
deep: true |
对象属性深层变化也触发(ref({...}) 默认只观察引用变化) |
最典型的就是在数据变化后发请求、保存缓存、做 DOM 操作:
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
// 每当 keyword 变化,自动保存一次搜索词
watch(keyword, (newVal, oldVal) => {
console.log('搜索词从', oldVal, '变为', newVal)
localStorage.setItem('keyword', newVal)
})
</script>
<template>
<input v-model="keyword" placeholder="输入搜索词" />
</template>
还可以一次监听多个来源(数组形式),或用 getter 监听某个具体属性:
// 监听多个
watch([keyword, count], ([newK, newC]) => {
console.log(newK, newC)
})
// 只监听对象的某个深层字段(不用 deep,更精准)
watch(() => user.value.age, (newAge) => {
console.log('年龄变为', newAge)
})
computed VS watch
computed 和 watch 能不能互相替代?不能直接替代。
computed 是派生数据(返回新值给模板用,有缓存);watch 是响应变化做副作用(不产出新值)。
经验法则:模板的计算一律用 computed,只有在”变化后做事情”时才用 watch。
watchEffect
watch 要手动告诉它”监听谁”;watchEffect 则什么都不用指定——它在回调执行时自动收集用到了哪些响应式数据,其中任何一个变了,就重新执行一次。
基本语法:watchEffect(回调)。
适合”同步副作用”、又不想一个个列依赖的场景:
import { watchEffect } from 'vue'
const keyword = ref('')
const page = ref(1)
// 不需要声明依赖,函数里用了谁它就听谁
watchEffect(() => {
document.title = `搜索:${keyword.value} - 第${page.value}页`
})
watch vs watchEffect
| 对比维度 | watch |
watchEffect |
|---|---|---|
| 依赖 | 手动指定监听目标 | 自动收集用到谁就听谁 |
| 拿到变化 | 有 newVal / oldVal |
没有新旧值对比 |
| 初始执行 | 默认不执行(加 immediate) |
立即执行一次 |
| 场景 | 精确监听某个变化、需要旧值做对比 | 同步副作用,不用管依赖,如 title |
| 异步操作 | 可以配合 await 发请求等 |
副作用里别做异步重活,保持轻量 |
一句话选择:
- 需要”这个变了→做那个”,用
watch - 如果只是”凑齐运行一遍,谁变了我重跑”,用
watchEffect。
速查卡片
| 场景 | 写法 |
|---|---|
| 派生数据(有缓存) | const t = computed(() => ...) |
| 读写都支持的派生值 | computed({ get, set }) |
| 监听变化做副作用 | watch(source, cb, { immediate, deep }) |
| 监听多个 / 某个字段 | watch([a, b], cb) / watch(() => obj.prop, cb) |
| 自动收集依赖的同步副作用 | watchEffect(() => { ... }) |