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

之前有一篇文章讲述了 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 中直接处理过多的业务逻辑,避免引发意外的依赖问题。

相关推荐
user20585561518135 分钟前
X6 中边悬浮置顶,规避 `mouseleave` 事件丢失问题
前端
李明卫杭州6 分钟前
CSS aspect-ratio 属性完全指南
前端
Pedantic2 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘2 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆2 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师3 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆3 小时前
VSCode自动格式化三要素
前端
爱勇宝4 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen5 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程