watch 和 watchEffect 的隐藏点 --- 非常细致

之前有一篇文章讲述了 watch 和 watchEffect 的使用,但在实际使用中,仍然存在一些“隐藏点”,可能会影响开发,在这补充一下。

1. watch 的隐藏点

1.1 性能陷阱:深度监听的影响

当在 watch 中使用 deep: true 来监听一个复杂的嵌套对象时,Vue 会递归遍历对象的所有属性,判断其是否发生变化。对于大规模或复杂的嵌套对象,这会产生显著的性能开销。尤其是在大型应用中,频繁使用深度监听可能导致性能下降。

1.2 初始值的处理

watch 默认是在监听的数据发生变化时执行,但可以通过设置 immediate: true 在初始化时立即执行一次回调,而不等待依赖项第一次变化后执行。这对于需要初始化时触发某些逻辑(如数据加载)的场景很有帮助。

然而,需要注意的是,immediate 执行时,旧值会是 undefined,这可能需要在回调中处理。

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 或者单独监听数组元素。

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 

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

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

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 的区别主要体现在视图更新的时机上,而不是数据更新的时机。由于视图更新还没有真正发生,所以从表面上看,二者的执行时机并没有明显的区别。

为了更好的看到变化,在控制台改变数据。

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

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 监听多个依赖时,必须注意依赖的声明顺序。如果将多个依赖组合在一起使用(如数组形式),回调函数只会在任一依赖变化时触发,而不会区分具体哪个依赖发生了变化。 

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 返回一个停止监听的函数,用于手动停止监听,但在复杂场景中,开发者容易忽略在组件销毁时手动调用停止函数,可能导致未预期的副作用持续存在。

const stop = watchEffect(() => {
  console.log('Running effect');
  // 假设这里创建了一些副作用(如定时器、网络请求)
});

stop(); //  需要在适当时机调用 stop 停止侦听
2.3 执行顺序与依赖追踪 

watchEffect 会在组件渲染后立即执行,并自动追踪所有在函数中读取的响应式数据。当这些数据发生变化时,watchEffect 将重新执行整个函数。

注意⚠️,watchEffect 在执行时可能会比组件的生命周期钩子(如 onMounted)更早,这可能导致依赖项尚未完全初始化,某些预期的行为未发生。

举个 🌰

<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 即可。

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 中直接处理过多的业务逻辑,避免引发意外的依赖问题。

  • 26
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值