之前有一篇文章讲述了 watch 和 watchEffect 的使用,但在实际使用中,仍然存在一些"隐藏点",可能会影响开发,在这补充一下。
1. watch 的隐藏点
1.1 性能陷阱:深度监听的影响
当在 watch 中使用 deep: true 来监听一个复杂的嵌套对象时,Vue 会递归遍历对象的所有属性,判断其是否发生变化。对于大规模或复杂的嵌套对象,这会产生显著的性能开销。尤其是在大型应用中,频繁使用深度监听可能导致性能下降。
1.2 初始值的处理
watch 默认是在监听的数据发生变化时执行,但可以通过设置 immediate: true 在初始化时立即执行一次回调,而不等待依赖项第一次变化后执行。这对于需要初始化时触发某些逻辑(如数据加载)的场景很有帮助。
然而,需要注意的是,immediate 执行时,旧值会是 undefined,这可能需要在回调中处理。
javascript
import { ref, watch } from 'vue'
const count = ref(0)
watch(
() => count.value,
(newValue, oldValue) => {
if (oldValue === undefined) {
console.log('oldValue is undefined')
}
console.log(`count changed from ${oldValue} to ${newValue}`)
},
{
immediate: true
}
)
count.value++
1.3 数组监听的行为
watch 默认对数组 的变化进行**"浅层"**监听,即只有当数组的引用发生变化时才会触发回调。如果需要监听数组内部元素的变化,则需要使用 deep: true 或者单独监听数组元素。
javascript
import { ref, watch } from 'vue'
const numbers = ref([1, 2, 3])
watch(
() => numbers.value,
(newVal, oldVal) => {
console.log('Array changed:', newVal)
},
{ deep: true }
)
numbers.value.push(4)
如果不使用 deep: true,watch 函数不会执行,但页面仍然改变,因为 numbers 是响应式数据。
1.4 延迟执行:flush
flush 选项 :watch 默认在组件更新后执行回调(flush: 'post'),但可以通过设置 flush 选项来改变这一行为:
-
pre:在 DOM 更新之前执行回调。
-
post:在 DOM 更新之后执行回调(默认行为)。
-
sync:在依赖项变化时立即同步执行回调。
这种设置可以帮助我们在特定的场景下更好地控制回调的执行时机,避免副作用与 DOM 更新过程的冲突。
举个 🌰
1)使用 post
javascript
import { ref, watch } from 'vue'
const state = ref({ count: 1 })
watch(
() => state.value.count,
(newVal) => {
console.log('依赖项更改后立即执行,newVal: ', newVal)
},
// 默认
{ flush: 'post' }
)
console.log('start')
state.value.count++
console.log('end')
2)使用 sync
javascript
import { ref, watch } from 'vue'
const state = ref({ count: 1 })
watch(
() => state.value.count,
(newVal) => {
console.log('依赖项更改后立即执行,newVal: ', newVal)
},
{ flush: 'sync' }
)
console.log('start')
state.value.count++
console.log('end')
3)使用 pre
javascript
import { ref, watch } from 'vue'
const state = ref({ count: 1 })
watch(
() => state.value.count,
(newVal) => {
console.log('依赖项更改后立即执行,newVal: ', newVal)
},
{ flush: 'pre' }
)
console.log('start')
state.value.count++
console.log('end')
看到 👀 这里可能感觉很疑惑,为什么 flush : pre 和 post 的结果一样呢?
在这个代码中,所有的操作都是同步执行的。state.value.count++ 是同步的赋值操作,而 console.log 也是同步的。所以当 state.value.count++ 执行时,watch 已经在收集依赖并准备好在数据变化后执行回调了。
Vue 采用的是异步 DOM 更新机制 ,也就是说,DOM 的更新是批处理的,通常在事件循环的下一次 tick 中才会实际执行。因此,pre 和 post 的区别主要体现在视图更新的时机上,而不是数据更新的时机。由于视图更新还没有真正发生,所以从表面上看,二者的执行时机并没有明显的区别。
为了更好的看到变化,在控制台改变数据。
javascript
import { ref, watch } from 'vue'
const state = ref({ count: 1 })
watch(
() => state.value.count,
(newVal) => {
console.log('依赖项更改后立即执行,newVal: ', newVal)
console.log('DOM内容:', document.querySelector('div').textContent)
},
{ flush: 'pre' }
)
window.state = state
javascript
import { ref, watch } from 'vue'
const state = ref({ count: 1 })
watch(
() => state.value.count,
(newVal) => {
console.log('依赖项更改后立即执行,newVal: ', newVal)
console.log('DOM内容:', document.querySelector('div').textContent)
},
{ flush: 'post' }
)
window.state = state
1.5 多个依赖
当使用 watch 监听多个依赖时,必须注意依赖的声明顺序。如果将多个依赖组合在一起使用(如数组形式),回调函数只会在任一依赖变化时触发,而不会区分具体哪个依赖发生了变化。
javascript
import { ref, watch } from 'vue'
const count = ref(0);
const message = ref('Hello World');
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
console.log('Either count or message changed');
console.log('New count:', newCount, 'Old count:', oldCount);
console.log('New message:', newMessage, 'Old message:', oldMessage);
});
window.count = count
window.message = message
2. watchEffect 的隐藏点
2.1 未提供旧值
与 watch 不同,watchEffect 无法获取依赖项的旧值,因为它的设计是为了简化依赖追踪和副作用处理。如果逻辑依赖于新旧值的对比,改用 watch 解决。
2.2 停止侦听的管理
watchEffect 返回一个停止监听的函数,用于手动停止监听,但在复杂场景中,开发者容易忽略在组件销毁时手动调用停止函数,可能导致未预期的副作用持续存在。
javascript
const stop = watchEffect(() => {
console.log('Running effect');
// 假设这里创建了一些副作用(如定时器、网络请求)
});
stop(); // 需要在适当时机调用 stop 停止侦听
2.3 执行顺序与依赖追踪
watchEffect 会在组件渲染后立即执行,并自动追踪所有在函数中读取的响应式数据。当这些数据发生变化时,watchEffect 将重新执行整个函数。
注意⚠️,watchEffect 在执行时可能会比组件的生命周期钩子(如 onMounted)更早,这可能导致依赖项尚未完全初始化,某些预期的行为未发生。
举个 🌰
javascript
<template>
<div>{{ data.message }}</div>
</template>
<script setup>
import { ref, watchEffect, onMounted } from 'vue';
const data = ref({
message: '',
});
watchEffect(() => {
console.log('watchEffect triggered, message:', data.value.message);
});
onMounted(() => {
console.log('Component mounted');
data.value.message = 'Hello, world!';
});
</script>
因为 watchEffect 在 onMounted 之前执行,也就是组件初次渲染之后立即执行,此时 message 为空字符串。只有当 onMounted 钩子函数触发并更改了 message 值后,watchEffect 再次触发,此时 message 为 'Hello,world'。
如果想避免该情况,使用 watch 即可。
javascript
watch(() => data.value.message, (newValue) => {
console.log('message changed:', newValue);
});
3. 共同问题
尽量避免出现下面情况:
3.1. 嵌套监听
如果在一个 watch 或 watchEffect 中嵌套使用另一个监听器,需要小心管理它们之间的依赖关系和停止条件,以避免复杂的嵌套监听逻辑造成难以调试的错误。
3.2 意外的副作用顺序
由于 Vue 3 响应式系统是异步批处理的,多个 watch 或 watchEffect 的回调函数可能会在同一帧中被调度和执行。这可能导致副作用之间的执行顺序不一致,尤其是在处理多个依赖数据的变化时。
4. 实际使用建议
1、优先使用 watchEffect:在逻辑简单、无条件依赖、无需旧值对比的场景中,watchEffect 更为简洁高效。
2、使用 watch 处理复杂依赖:如果场景中需要深度监听、处理复杂逻辑、对比新旧值,或者避免无限循环时,优先选择 watch。
3、管理依赖和副作用:尽量将复杂的逻辑抽离出来,避免在 watchEffect 或 watch 中直接处理过多的业务逻辑,避免引发意外的依赖问题。