前言
Vue3 提供了三个处理响应式数据副作用的 API:computed、watch 和 watchEffect。很多开发者容易混淆它们的适用场景。本文将用对比的方式彻底讲清楚。
一、一句话区别
| API |
一句话 |
触发时机 |
| computed |
计算一个派生值,自动缓存 |
依赖的响应式数据变化时重新计算 |
| watch |
监听特定数据源,执行副作用 |
被监听的数据源变化时回调 |
| watchEffect |
自动追踪依赖,立即执行副作用 |
初始化立即执行 + 依赖变化重新执行 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { ref, computed, watch, watchEffect } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
watch(count, (newVal, oldVal) => { console.log(`${oldVal} → ${newVal}`) })
watchEffect(() => { console.log(`count 变了:${count.value}`) })
|
二、computed 详解
基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { ref, computed } from 'vue'
const firstName = ref('张') const lastName = ref('三')
const fullName = computed(() => firstName.value + lastName.value)
const fullName2 = computed({ get() { return firstName.value + ' ' + lastName.value }, set(val) { [firstName.value, lastName.value] = val.split(' ') } })
fullName2.value = '李 四'
|
核心特性:缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const count = ref(0)
const noCache = () => { console.log('计算了一次') return count.value * 2 }
const withCache = computed(() => { console.log('计算了一次') return count.value * 2 })
noCache(); noCache(); noCache() withCache.value; withCache.value; withCache.value
|
缓存条件:只有当依赖的响应式数据发生变化时,computed 才会重新计算,否则直接返回上一次的结果。
使用场景
| 场景 |
示例 |
| 数据格式化 |
价格格式化、日期格式化 |
| 列表过滤/排序 |
搜索过滤表格、按条件排序 |
| 多字段拼接 |
姓名拼接、地址拼接 |
| 条件判断 |
是否禁用按钮、是否展示内容 |
| 数据统计 |
总价/均价计算、数量统计 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <script setup> import { ref, computed } from 'vue'
// 购物车场景 const cartItems = ref([ { name: 'Vue3 实战', price: 59, count: 2 }, { name: 'JS 高级', price: 39, count: 1 } ])
// 总价 — computed 自动跟踪 cartItems 变化 const totalPrice = computed(() => cartItems.value.reduce((sum, item) => sum + item.price * item.count, 0) )
// 列表过滤 const keyword = ref('') const filteredItems = computed(() => cartItems.value.filter(item => item.name.includes(keyword.value)) ) </script>
|
三、watch 详解
基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { ref, reactive, watch } from 'vue'
const count = ref(0) watch(count, (newVal, oldVal) => { console.log(`count: ${oldVal} → ${newVal}`) })
const state = reactive({ count: 0, name: 'Vue' }) watch( () => state.count, (newVal, oldVal) => { console.log(newVal, oldVal) } )
const x = ref(0) const y = ref(0) watch([x, y], ([newX, newY], [oldX, oldY]) => { console.log(`(${oldX},${oldY}) → (${newX},${newY})`) })
|
深度监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const obj = reactive({ user: { name: '张三', profile: { age: 25 } } })
watch( () => obj.user, (newVal) => { console.log('user 内部变化了', newVal) }, { deep: true } )
watch(obj, (newVal) => { console.log('obj 任意属性变化都会触发') })
|
1 2 3 4 5 6 7
| const keyword = ref('')
watch(keyword, (newVal) => { fetchSearchResults(newVal) }, { immediate: true })
|
flush 选项:控制回调时机
1 2 3 4 5 6 7 8 9
| watch(source, callback, { flush: 'pre' })
watch(source, callback, { flush: 'post' })
watch(source, callback, { flush: 'sync' })
|
使用场景
| 场景 |
说明 |
| 路由监听 |
watch(() => route.params.id, fetchData) |
| 搜索防抖 |
配合 debounce 监听 keyword 变化发请求 |
| 表单联动 |
省变化后自动请求市列表 |
| 新旧值对比 |
需要知道改之前的值是什么 |
| 分页请求 |
页码/页大小变化重新拉数据 |
| props 变化 |
子组件监听 props 变化做处理 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <script setup> import { ref, watch } from 'vue' import { useRoute } from 'vue-router'
const route = useRoute()
// 路由参数变化时重新请求 watch(() => route.params.id, (newId) => { fetchArticle(newId) })
// 搜索防抖 const keyword = ref('') let timer = null watch(keyword, (val) => { clearTimeout(timer) timer = setTimeout(() => fetchSearch(val), 300) }) </script>
|
四、watchEffect 详解
基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { ref, watchEffect } from 'vue'
const count = ref(0) const name = ref('Vue')
watchEffect(() => { console.log(`count: ${count.value}, name: ${name.value}`) })
count.value++ name.value = 'Vue3'
|
核心逻辑:watchEffect 会自动追踪回调函数中用到的所有响应式数据,任何一个变化都会重新执行。
清除副作用(onCleanup)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const userId = ref(1)
watchEffect((onCleanup) => { let cancelled = false
onCleanup(() => { cancelled = true })
fetchUser(userId.value).then(data => { if (!cancelled) { console.log('用户信息:', data) } }) })
setTimeout(() => userId.value = 2, 1000)
|
典型场景:userId 快速切换时,上一次的异步请求还没返回,用 onCleanup 丢弃过期结果,防止数据错乱。
停止监听
1 2 3 4 5
| const stop = watchEffect(() => { })
stop()
|
使用场景
| 场景 |
说明 |
| 自动追踪多依赖 |
不用手动指定依赖列表 |
| 初始化立即执行 |
页面加载就要运行的副作用 |
| 竞态请求 |
配合 onCleanup 取消过期请求 |
| 非侵入式日志 |
调试时追踪多个状态的变化 |
五、三者的区别对比
核心对比表
| 维度 |
computed |
watch |
watchEffect |
| 返回值 |
返回一个 ref(只读) |
无返回值 |
无返回值 |
| 缓存 |
✅ 有缓存 |
❌ 无 |
❌ 无 |
| 首次执行 |
惰性(访问时才计算) |
❌ 不执行(除非 immediate: true) |
✅ 立即执行 |
| 依赖追踪 |
自动 |
手动指定 |
自动 |
| 新旧值 |
❌ 不提供 |
✅ (newVal, oldVal) |
❌ 不提供 |
| 深度监听 |
自动(getter 中访问到的) |
手动 { deep: true } |
自动(回调中访问到的) |
| 副作用清理 |
❌ |
✅ onCleanup |
✅ onCleanup |
| DOM 更新时机 |
同步 |
可控 flush |
可控 flush |
| 可写 |
✅ 支持 setter |
❌ |
❌ |
执行时机图示
1 2 3 4 5
| computed: 依赖变化 → 标记 dirty → 下次访问 .value 时才重新计算
watch: 依赖变化 → 满足条件 → 执行回调(默认 DOM 更新前)
watchEffect: 初始化立即执行 ─→ 依赖变化 → 执行回调(默认 DOM 更新前)
|
选择决策树
1 2 3 4 5 6 7 8 9 10
| 需要返回一个值? ├─ 是 → 用 computed │ ├─ 只读 → computed(() => ...) │ └─ 可写 → computed({ get, set }) │ └─ 否 → 需要执行副作用? ├─ 需要拿到旧值?→ watch ├─ 需要懒执行(不立即触发)?→ watch ├─ 需要监听特定数据源(路由/props)?→ watch └─ 自动追踪 + 立即执行?→ watchEffect
|
六、常见误区
误区 1:computed 里做副作用
1 2 3 4 5 6 7 8 9 10 11 12
| const data = computed(() => { fetch('/api/list').then(...) document.title = '新标题' return list.value.filter(...) })
const data = computed(() => list.value.filter(...)) watchEffect(() => { document.title = `共 ${data.value.length} 条` })
|
误区 2:watchEffect 里修改自身依赖导致死循环
1 2 3 4 5 6 7 8 9 10
| const count = ref(0) watchEffect(() => { count.value++ })
watchEffect(() => { if (count.value < 10) count.value++ })
|
误区 3:watch reactive 对象属性不写 getter
1 2 3 4 5 6 7
| const state = reactive({ count: 0 })
watch(state.count, (val) => console.log(val))
watch(() => state.count, (val) => console.log(val))
|
误区 4:用 watch 代替 computed
1 2 3 4 5 6
| const double = ref(0) watch(count, (val) => { double.value = val * 2 })
const double = computed(() => count.value * 2)
|
七、原理简析
computed 的 dirty 标记
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function computed(getter) { let value let dirty = true
const effect = () => { value = getter() dirty = false }
return { get value() { if (dirty) { effect() } return value } } }
|
watchEffect 的自动追踪
1 2 3 4 5 6 7 8 9
| function watchEffect(fn) { const effect = () => { activeEffect = effect fn() activeEffect = null } effect() }
|
watch 和 watchEffect 底层都依赖 ReactiveEffect,区别在于 watch 手动指定依赖,watchEffect 自动追踪。
八、面试回答思路
问:”computed、watch、watchEffect 有什么区别?”
建议回答框架:
- 先给一句话定义:computed 是计算派生值有缓存,watch 是手动监听特定源,watchEffect 是自动追踪+立即执行
- 说核心差异:缓存(computed 有,watch 没有)、旧值(watch 有,computed/watchEffect 没有)、首次执行(watchEffect 立即,computed 惰性,watch 默认不执行)
- 举典型场景:
- computed → 格式化、过滤、统计
- watch → 路由监听、搜索防抖、props 变化
- watchEffect → 多依赖自动追踪、竞态清理
- 提一下原理:computed 基于 dirty 标记实现缓存;watch 和 watchEffect 底层都是 ReactiveEffect
总结
| 选型 |
条件 |
| computed |
需要基于现有数据计算一个新值,且希望自动缓存 |
| watch |
需要知道具体哪个值变了(新旧值),或只监听特定源不立即执行 |
| watchEffect |
不关心具体哪个值变了,只需自动追踪依赖并在变化时执行副作用 |
记住这三句话就不会选错:算缓存用 computed,追旧值用 watch,自动跑用 watchEffect。
内容由 AI 生成,仅供参考。本文发布于 码上学习,转载请注明出处。