图秀主页
56.67M · 2026-02-04
在Vue3中,侦听器(Watch)是一种强大的工具,用于在响应式状态变化时执行副作用操作。当你需要在数据变化时执行异步操作、更新DOM或者修改其他状态时,侦听器就派上用场了。它与计算属性不同,计算属性主要用于声明式地计算衍生值,而侦听器更适合处理副作用逻辑。
export default {
data() {
return {
question: '',
answer: 'Questions usually contain a question mark. ;-)',
loading: false
}
},
watch: {
// 每当 question 改变时,这个函数就会执行
question(newQuestion, oldQuestion) {
if (newQuestion.includes('?')) {
this.getAnswer()
}
}
},
methods: {
async getAnswer() {
this.loading = true
this.answer = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
this.answer = (await res.json()).answer
} catch (error) {
this.answer = 'Error! Could not reach the API. ' + error
} finally {
this.loading = false
}
}
}
}
<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)
// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
} finally {
loading.value = false
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
watch的第一个参数可以是不同形式的数据源:
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
watch默认是浅层的:被侦听的属性仅在被赋新值时才会触发回调函数,而嵌套属性的变化不会触发。如果想侦听所有嵌套的变更,你需要使用深层侦听器。
export default {
watch: {
someObject: {
handler(newValue, oldValue) {
// 注意:在嵌套的变更中,只要没有替换对象本身,newValue和oldValue相同
},
deep: true
}
}
}
const obj = reactive({ count: 0 })
// 直接传入响应式对象,隐式创建深层侦听器
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:newValue和oldValue是相等的,因为它们是同一个对象!
})
obj.count++
深度侦听需要遍历被侦听对象中的所有嵌套属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。在Vue 3.5+中,deep选项还可以是一个数字,表示最大遍历深度。
watch默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时立即执行一遍回调,比如请求初始数据。
export default {
watch: {
question: {
handler(newQuestion) {
// 在组件实例创建时会立即调用
},
// 强制立即执行回调
immediate: true
}
}
}
watch(
source,
(newValue, oldValue) => {
// 立即执行,且当source改变时再次执行
},
{ immediate: true }
)
一次性侦听器仅在被侦听源第一次变化时触发回调,之后自动停止侦听。这个特性在Vue 3.4及以上版本支持。
export default {
watch: {
source: {
handler(newValue, oldValue) {
// 当source变化时,仅触发一次
},
once: true
}
}
}
watch(
source,
(newValue, oldValue) => {
// 当source变化时,仅触发一次
},
{ once: true }
)
watchEffect允许我们自动跟踪回调的响应式依赖,它会立即执行,不需要指定immediate: true。在执行期间,它会自动追踪所有能访问到的响应式属性,每当这些属性变化时,回调会再次执行。
const todoId = ref(1)
const data = ref(null)
watchEffect(async () => {
const response = await fetch(
`${todoId.value}`
)
data.value = await response.json()
})
watchEffect仅会在其同步执行期间追踪依赖。在使用异步回调时,只有在第一个await正常工作前访问到的属性才会被追踪。
| 特性 | watch | watchEffect |
|---|---|---|
| 追踪方式 | 只追踪明确侦听的数据源 | 在副作用发生期间自动追踪所有能访问到的响应式属性 |
| 执行时机 | 懒执行,仅在数据源变化时触发 | 立即执行,之后在依赖变化时再次执行 |
| 回调参数 | 可以获取新旧值 | 无法直接获取新旧值 |
| 使用场景 | 需要精确控制触发时机,或者需要获取新旧值时 | 多个依赖项的侦听器,或者不需要关心具体变化值时 |
当我们在侦听器中执行异步操作时,可能会出现竞态问题。例如,在请求完成之前数据源发生了变化,当上一个请求完成时,它仍会使用已经过时的数据触发回调。这时我们需要清理这些过时的副作用。
watch(id, (newId, oldId, onCleanup) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// 回调逻辑
})
onCleanup(() => {
// 终止过期请求
controller.abort()
})
})
默认情况下,侦听器回调会在父组件更新之后、所属组件的DOM更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的DOM,那么DOM将处于更新前的状态。
如果想在侦听器回调中能访问被Vue更新之后的所属组件的DOM,你需要指明flush: 'post'选项:
watch(source, callback, {
flush: 'post'
})
// 或者使用watchPostEffect
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在Vue更新后执行 */
})
你还可以创建一个同步触发的侦听器,它会在Vue进行任何更新之前触发:
watch(source, callback, {
flush: 'sync'
})
// 或者使用watchSyncEffect
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* 在响应式数据变化时同步执行 */
})
我们也可以使用组件实例的$watch方法来命令式地创建一个侦听器。这在特定条件下设置侦听器,或者只侦听响应用户交互的内容时很有用。
export default {
created() {
this.$watch('question', (newQuestion) => {
// ...
})
}
}
用watch选项或者$watch()实例方法声明的侦听器,会在宿主组件卸载时自动停止。在setup()或script setup中用同步语句创建的侦听器,也会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。
在少数情况下,你需要在组件卸载之前就停止一个侦听器,这时可以调用$watch() API返回的函数:
const unwatch = this.$watch('foo', callback)
// ...当该侦听器不再需要时
unwatch()
如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏:
<script setup>
import { watchEffect } from 'vue'
// 这个需要手动停止
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
</script>
答案解析: 有两种方式可以侦听响应式对象的嵌套属性变化:
watch(
() => obj.nestedProperty,
(newValue) => {
console.log(newValue)
}
)
watch(obj, (newValue) => {
console.log(newValue.nestedProperty)
}, { deep: true })
注意:深层侦听器会遍历对象的所有嵌套属性,性能开销较大,建议优先使用getter函数的方式。
答案解析:
答案解析: 可以通过onCleanup函数来清理副作用,它作为第三个参数传递给watch回调函数,或者作为第一个参数传递给watchEffect的作用函数:
watch(id, (newId, oldId, onCleanup) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// 回调逻辑
})
onCleanup(() => {
// 终止过期请求
controller.abort()
})
})
产生原因: 在侦听器回调中访问了未初始化的响应式属性。
解决办法: 在访问属性之前先进行判断:
watchEffect(() => {
if (data.value) {
console.log(data.value.xxx)
}
})
产生原因: 在侦听器回调中修改了被侦听的数据源,导致无限循环。
解决办法: 避免在侦听器回调中直接修改被侦听的数据源,或者使用条件判断来终止循环:
watch(count, (newCount) => {
if (newCount < 10) {
count.value++
}
})
产生原因: watch的第一个参数不是有效的数据源类型,比如直接传递了响应式对象的属性值。
解决办法: 使用getter函数来返回响应式对象的属性:
// 错误写法
watch(obj.count, (count) => {
console.log(count)
})
// 正确写法
watch(
() => obj.count,
(count) => {
console.log(count)
}
)
cn.vuejs.org/guide/essen…