Appearance
侦听器 watch
基本示例
计算属性可以让我们声明性的计算衍生值,但也有不可以有副作用的约定,侦听器则允许我们执行异步操作或较大开销的操作,而不必考虑计算属性的限制
html
<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的第一个参数可以是不同形式的"数据源",包括:
- 一个 ref
- 一个响应式对象
- 一个 getter 函数
- 包含上述类型的一个数组
javascript
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}`)
})但是不可以直接侦听响应式对象的属性值 ,因为这样侦听不到属性值的变化
javascript
const obj = reactive({ count: 0 })
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`Count is: ${count}`)
})为什么???
这里需要明白Vue3响应式的本质.响应式对象是通过Proxy实现的,这里声明的obj就是{count:0}这个对象的代理, 可以理解为 : obj = new Proxy({count:0}).在通过obj访问属性时,Vue会拦截这个操作,并返回属性值, 所以obj.count的值是0,而不是一个响应式的引用,因此无法侦听到变化.
如何解决???
可以通过getter函数来访问响应式对象的属性值,这样Vue就会将这个函数的返回值作为响应式数据,从而侦听到变化
javascript
const obj = reactive({ count: 0 })
watch(
() => obj.count, // 通过getter函数访问响应式对象的属性值
(count) => {
console.log(`Count is: ${count}`)
},
)在watch的上下文中,Getter函数就是传入watch的第一个参数的函数,Vue在执行这个函数时才会访问obj.count的值,内部通过 Proxy建立依赖关系 这里是对源码的一点分析
深层侦听器
直接给watch()传入一个响应式对象,会隐私的创建一个生成侦听器--该回调函数在所有嵌套变更时都会被处罚:
javascript
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发 => count 属性的变化
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++相比较之下,一个返回响应时对象的getter函数只会在该对象的引用发生变化时触发:
javascript
const obj = reactive({ count: 0 })
watch(
() => obj.count,
(newValue, oldValue) => {
// 在嵌套的属性变更时不会触发
// 只有当 obj.count发生变化时才会触发
},
)
obj.count++也可以在使用watch()时显式地启用深层侦听器,通过传入一个选项对象:
javascript
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true },
)在Vue 3.5+中,deep选项还可以是一个数字,表示侦听的深度--既Vue应该遍历对象嵌套属性的级数
深度监听在用于大型数据结构时开销很大,因此请谨慎使用,并确保只在必要时使用
即时回调的侦听器
watch默认是懒执行的:仅当数据源变化时才会执行回调.如果需要在侦听器创建时立即执行回调,可以传入一个选项对象,并将 immediate设置为true
javascript
watch(
() => state.someValue,
(newValue, oldValue) => {
// 回调将在侦听器创建时立即执行
},
{ immediate: true },
)一次性侦听
仅支持 3.4+
如果只需要侦听一次数据源的变化,可以使用
once选项,它会在第一次变化时执行回调,然后自动取消侦听
javascript
watch(
() => state.someValue,
(newValue, oldValue) => {
// 回调将在第一次变化时执行
},
{ once: true },
)watchEffect()
侦听器的回调使用与源完全相同的响应式状态是很常见的:
javascript
const todoId = ref(1)
const data = ref(null)
watch(
todoId,
async () => {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)
data.value = await response.json()
},
{ immediate: true },
)watchEffect()是一个更简洁的API,它会立即执行回调,并在回调中使用的任何响应式状态发生变化时重新执行回调
javascript
import { ref, watchEffect } from 'vue'
const todoId = ref(1)
const data = ref(null)
watchEffect(async () => {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)
data.value = await response.json()
})这个例子中,回调会立即执行,并在todoId变化时重新执行,因为todoId是响应式的. watchEffect()的这种特征在有多个依赖项的侦听器来说,可以消除手动维护依赖列表的负担.
此外,如果你需要侦听搞一个嵌套数据结构中的几个属性,watchEffect()可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归的跟踪所有属性
javascript
const user = reactive({
id: 1,
profile: {
name: 'Alice',
address: {
city: 'Shanghai',
district: 'Pudong',
},
},
preferences: {
theme: 'dark',
},
})- 常规侦听
javascript
watch(
() => user.profile,
(newVal) => {
console.log('城市变化:', newVal.address.city)
},
{ deep: true }, // 会递归追踪整个 profile 对象
)常规侦听中,如果修改的是address属性,也会触发回调
- 使用
watchEffect()
javascript
watchEffect(() => {
console.log('城市变化:', user.profile.address.city)
})使用watchEffect()时,只会追踪回调中使用到的属性,如果修改的是preferences属性,则不会触发回调
最佳实践建议
TIP
watchEffect()仅会在其同步执行期间才追踪依赖.在使用会不会掉时,只有在第一个await正常工作前访问到的属性才会被追踪
1.精准追踪:对于嵌套对象优先使用watchEffect,只追踪实际用到的属性
2.简单路径:如果需要侦听简单路径,比如(user.id),可以使用watch而不是watchEffect,因为它更简洁
3.复杂场景:当需要显式控制侦听源时,在使用watch+deep
注意事项
- 在
watchEffect()中注意不要访问不需要追踪的属性- 对于需要出示自信的情况,
watchEffect()会自动给你寄执行一次- 如果确实需要侦听整个对象更多结构变化,才使用
deep
watchEffect()仅会在其同步执行期间才追踪依赖,
javascript
import { ref, watchEffect } from 'vue'
const count = ref(0)
const username = ref('')
watchEffect(async () => {
console.log(count.value) // 会被追踪
await fetch('/api') // 异步操作
console.log(username.value) // 不会被自动追踪!
})javascript
// 定时器中的依赖遗漏
const timer = ref(null)
const interval = ref(1000)
watchEffect(() => {
// 在同步阶段清除旧定时器(会被追踪)
clearTimeout(timer.value)
setTimeout(() => {
// 不会被追踪的访问
console.log(interval.value)
}, interval.value) // 只有这里的 interval.value 会被追踪
})最佳实践建议
1.最小化异步操作:在watchEffect()中尽量避免异步操作,尽量将异步操作移除watchEffect
2.同步阶段捕获所有需要的值
3.考虑使用watch+immediate选项来确保在侦听器创建时立即执行回调,而不是依赖于watchEffect()的自动执行4.使用组合式函数封装: 如useAsyncData等更高级的抽象
示例
1.基本拆分方式
javascript
import { ref, watchEffect } from 'vue'
const userId = ref(1)
const userData = ref(null)
// 同步部分处理依赖追踪
watchEffect(() => {
const currentUserId = userId.value // 同步捕获响应式值
// 异步操作封装到独立函数
const fetchUser = async () => {
userData.value = await fetch(`/api/users/${currentUserId}`).then((r) => r.json())
}
fetchUser()
})2.使用 async/await的优化拆分
javascript
const searchQuery = ref('')
const searchResults = ref([])
watchEffect(() => {
// 同步阶段:捕获所有需要的响应式值
const query = searchQuery.value(
// 立即执行异步函数(IIFE)
async () => {
if (query.length < 2) return
searchResults.value = await fetchResults(query)
},
)()
})3.使用组合式函数封装
javascript
// composables/useAsyncData.js
export function useAsyncData(getter, asyncFn) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
watchEffect(() => {
const params = getter() // 同步获取所有依赖项
loading.value = true
asyncFn(params)
.then((result) => (data.value = result))
.catch((err) => (error.value = err))
.finally(() => (loading.value = false))
})
return { data, loading, error }
}
// 组件中使用
import { useAsyncData } from './composables/useAsyncData'
const userId = ref(1)
const { data: user, loading } = useAsyncData(
() => ({ userId: userId.value }), // 同步getter
async (params) => {
return fetch(`/api/users/${params.userId}`).then((r) => r.json())
},
)4.响应式参数对象模式
javascript
const filters = reactive({
category: 'books',
query: '',
page: 1,
})
watchEffect(() => {
// 同步创建请求参数快照
const params = { ...toRefs(filters) }
fetchResults(params).then((data) => {
// 处理结果
})
})5.使用Promise链替代 async/await
javascript
const sessionToken = ref('')
const permissions = ref([])
watchEffect(() => {
const token = sessionToken.value
getSessionInfo(token)
.then((info) => checkPermissions(info.role))
.then((perms) => (permissions.value = perms))
.catch(console.error)
})watch vs watchEffect
两者都能响应式的执行有副作用的会掉,主要区别是追踪响应式依赖的方式
watch只能追踪明确侦听的数据源.仅在数据源发生改变时出发回调.会避免在发生副作用是追踪回调,因此能更加精确的控制回调函数的触发时机watchEffect会在副作用发生期间追踪依赖,会在同步执行过程中自动追踪所有能访问的响应式属性,这个特性会使代码更简洁,但响应式依赖的关系不太明确
副作用清理
有时我们可能会在侦听器中执行副作用,比如异步请求:
javascript
watch(id, (newId) => {
fetch(`/api/${newId}`).then(() => {
// 回调逻辑
})
})但是如果在请求过程中,id发生了变化,当请求完成时,它还会使用已经过期的id触发回调,正常徐秋霞,应该在id发生变化时取消过时请求 这里我们可以使用onWatcherCleanup()API注册一个清理函数,当侦听器失效并准备重新运行时会被调用
javascript
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// 回调逻辑
})
onWatcherCleanup(() => {
// 终止过期请求
controller.abort()
})
})注意 >
onWatcherCleanup仅在Vue 3.5+中支持,并且必须在watchEffect效果函数或watch回调函数的同步期间执行,不能在await语句之后调用
作为替代,onClean函数可以作为第三个参数传递给监听器回调,以及watchEffect作用函数的第一个参数 :
javascript
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// 清理逻辑
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// 清理逻辑
})
})这在3.5之前的版本有效,此外,通过函数参数传递的onCleanup与侦听器实例绑定,因此不受onWatcherCleanup的同步限制
回调的触发时机
当响应式状态发生改变时,可能同时触发Vue组件的更新和侦听器的回调
类似于组件更新,用户创建的侦听器回调函数也会被批量处理以避免重复调用.默认情况下,侦听器回调会在父组件更新(如果有)之后,所属组件的 DOM更新之前调用,也就是说 如果你在侦听器中访问所属组件的DOM,那么你拿到的DOM将处于更新之前的状态
Post Watcher
如果想在侦听器的回调中能访问被Vue更新之后的所属组件DOM,需要知名flush:'post' :
javascript
watch(source, callback, {
flush: 'post',
})
watchEffect(callback, {
flush: 'post',
})针对watchEffect还有个更方便的别名watchPostEffect() :
javascript
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})同步侦听器
同步侦听器会在Vue进行更新之前出发,通过指定配置flush:'sync' :
javascript
watch(source, callback, {
flush: 'sync',
})
watchEffect(callback, {
flush: 'sync',
})同样也有个别名watchSyncEffect() :
javascript
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* 在响应式数据变化时同步执行 */
})谨慎使用
同步侦听器不会进行批处理,每当响应式数据发生变化时就会出发.可以用它来监视简单的布尔值,但应该避免在可能多次同步修改的数据源上使用
停止侦听器
在setup或script setup中使用同步语句创建的监听器会自动绑定到宿主组件的实力上,并会在组件卸载的时候自动停止
一个关键点时:侦听器必须用同步语句创建,否则不会自动停止,这时就需要手动停止,防止内存泄露:
html
<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>通过watch或watchEffect返回的函数来停止侦听器:
javascript
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
//数据加载后执行某些操作...
}
})