/
Update
6 min read
中文 watch 和 watchEffect
一、核心区别#
| 特性 | watch | watchEffect |
|---|---|---|
| 监听方式 | 显式指定要监听的数据源 | 自动追踪回调中用到的响应式数据 |
| 首次执行 | 默认不执行,数据变化后才触发 | 立即执行(类似 computed) |
| 获取旧值 | ✅ 可以获取 oldValue | ❌ 无法获取旧值 |
| 使用场景 | 需要对比新旧值、条件触发 | 副作用逻辑(如日志、DOM 操作) |
| 写法 | 需要指定数据源 | 无需指定,自动依赖收集 |
二、watch 详解#
基本用法#
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const name = ref('Vue')
// 监听单个 ref
watch(count, (newVal, oldVal) => {
console.log('count 变化:', oldVal, '→', newVal)
})
// 监听多个数据源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log('多个数据变化:', newCount, newName)
})
// 监听 getter 函数(监听对象属性)
const user = ref({ age: 20 })
watch(
() => user.value.age,
(newAge, oldAge) => {
console.log('age 变化:', oldAge, '→', newAge)
}
)
</script>vue配置选项#
watch(source, callback, {
immediate: true, // 首次立即执行(默认 false)
deep: true, // 深度监听对象内部变化(默认 false)
flush: 'post' // 组件更新后执行(默认 'pre')
})javascript深度监听#
const state = reactive({
nested: { count: 0 }
})
// ❌ 浅监听:只监听引用变化
watch(state.nested, (newVal, oldVal) => {
console.log('不会触发,除非整个对象替换')
})
// ✅ 深度监听
watch(
() => state.nested,
(newVal, oldVal) => {
console.log('嵌套属性变化也会触发')
},
{ deep: true }
)javascript⚠️ 深度监听性能开销大,尽量精确监听具体属性。
三、watchEffect 详解#
基本用法#
<script setup>
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('Vue')
// 自动追踪用到的响应式数据
watchEffect(() => {
console.log('当前 count:', count.value)
console.log('当前 name:', name.value)
// 只要 count 或 name 变化,就会重新执行
})
// 首次立即输出:
// 当前 count: 0
// 当前 name: Vue
</script>vue清理副作用#
const id = ref(1)
watchEffect((onCleanup) => {
const controller = new AbortController()
fetch(`/api/user/${id.value}`, { signal: controller.signal })
.then(res => res.json())
.then(data => console.log(data))
// 下次执行前或组件卸载时调用
onCleanup(() => {
controller.abort() // 取消之前的请求
})
})javascript停止监听#
const stop = watchEffect(() => {
console.log(count.value)
})
// 手动停止
stop()javascript四、对比示例#
场景:监听搜索关键词#
<script setup>
import { ref, watch, watchEffect } from 'vue'
const keyword = ref('')
const results = ref([])
// ✅ watch:适合有明确数据源,需要对比新旧值
watch(keyword, async (newVal, oldVal) => {
if (newVal.length < 2) {
results.value = []
return
}
const data = await fetchSearch(newVal)
results.value = data
}, { debounce: 300 }) // 可配合防抖
// ✅ watchEffect:适合副作用逻辑,自动追踪依赖
watchEffect(() => {
console.log(`搜索日志: 用户输入 "${keyword.value}"`)
document.title = keyword.value
? `搜索: ${keyword.value}`
: '首页'
})
</script>vue五、何时用哪个?#
┌─────────────────────────────────────────────────────────┐
│ 需要获取旧值做对比? │
│ 例如:撤销功能、表单对比 │
└────────────┬────────────────────────────────────────────┘
│
┌──────┴──────┐
是 否
│ │
▼ ▼
用 watch 副作用逻辑?
│
┌─────┴─────┐
是 否
│ │
▼ ▼
watchEffect 明确数据源?
│
┌──────┴──────┐
是 否
│ │
▼ ▼
用 watch 用 watchEffectplaintext用 watch | 用 watchEffect |
|---|---|
需要 oldValue | 副作用逻辑(日志、DOM、标题) |
| 需要条件判断(如防抖) | 依赖关系复杂,不想手动维护 |
| 监听特定属性 | 需要立即执行 |
需要 deep 深度监听 | 请求取消等清理副作用 |
六、常见误区#
误区 1:watch 监听 reactive 对象#
const state = reactive({ count: 0 })
// ❌ 错误:监听的是 reactive 对象,不是 ref
watch(state, () => {}) // 可以工作,但无法正确获取旧值
// ✅ 正确:监听 getter 或转为 ref
watch(() => state.count, (newVal, oldVal) => {})
watch(toRef(state, 'count'), (newVal, oldVal) => {})javascript误区 2:watchEffect 中遗漏依赖#
const count = ref(0)
const name = ref('Vue')
watchEffect(() => {
console.log(count.value) // 只追踪了 count
// name 变化不会触发!
})javascript误区 3:异步回调中的旧值陷阱#
watch(count, async (newVal, oldVal) => {
await someAsyncOp() // 等待期间 count 可能又变了
console.log(oldVal) // 还是正确的旧值 ✅
})javascript七、Vue 2 vs Vue 3 的 watch#
| Vue 2 (Options API) | Vue 3 (Composition API) | |
|---|---|---|
| 写法 | watch: { count() {} } | watch(count, () => {}) |
| 立即执行 | immediate: true | immediate: true |
| 深度监听 | deep: true | deep: true |
| 多数据源 | 需要写多个 watcher | watch([a, b], ...) |
| watchEffect | ❌ 无 | ✅ 有 |
八、总结#
watch | watchEffect | |
|---|---|---|
| 本质 | 命令式:你告诉 Vue 监听什么 | 声明式:Vue 自动发现依赖 |
| 心智模型 | ”当 X 变化时,做 Y" | "这段代码依赖了哪些数据,就自动重跑” |
| 灵活性 | 高(精确控制) | 低(自动但不可控) |
| 性能 | 可控(只监听需要的数据) | 需注意(可能追踪过多依赖) |
一句话记忆:
watch是精准狙击——你指定目标,它才开火;
watchEffect是自动雷达——它自己扫描范围内的所有响应式数据,谁动打谁。