Vue3 watchEffect 进阶使用指南:这些特性你可能不知道

系列文章目录

Vue3 组合式 API 进阶:深入解析 customRef 的设计哲学与实战技巧
Vue3 watchEffect 进阶使用指南:这些特性你可能不知道
Vue3高级特性:深入理解effectScope及其应用场景



前言

作为 Vue3 响应式系统的 “侦察兵”,watchEffect 总能精准捕捉依赖变化并执行副作用。但多数开发者可能只停留在 “创建即忘” 的基础用法,殊不知它藏着不少能提升代码质量的进阶特性。本文就带你深挖这些 “冷知识”,让你的副作用管理更优雅、更高效。

在这里插入图片描述


一、别让 “侦察兵” 带薪摸鱼:主动停止监听

我们都知道 watchEffect 会自动追踪响应式依赖,但你知道它还能 “急流勇退” 吗?当调用 watchEffect 时,它会返回一个停止函数,这个函数能帮我们在合适的时机手动终止监听。

刚接触 watchEffect 时,你可能会写出这样的代码:

import { ref, watchEffect } from 'vue'

const count = ref(0)
watchEffect(() => {
  console.log(`count变化了:${count.value}`)
})

这段代码会在 count 变化时持续打印日志,但你知道吗?watchEffect 会返回一个停止函数,当你不再需要监听时,可以主动 “解雇” 这个侦察兵:

const stop = watchEffect(() => {
  console.log(`count变化了:${count.value}`)
})

// 3秒后停止监听
setTimeout(stop, 3000)

何时需要手动调用停止函数呢 ?

1、组件卸载时自动停止

当在组件<script setup 语法糖顶层或者 setup()写法函数内 或生命周期钩子中同步调用 watchEffect 时,Vue 会自动在组件卸载时停止侦听器,无需手动调用。

例如:

setup() {
  // 同步调用:组件卸载时自动停止
  watchEffect(() => {
    console.log('自动停止的侦听器')
  })
}
<script setup>
   onMounted(() => {
   //生命周期钩子中同步调用,组件卸载时自动停止
      watchEffect(() => { /* ... */ })    
  })
</script>

2、需要手动调用停止函数的情况:

(1)异步创建的侦听器

在 setTimeout、Promise 或异步操作中创建 watchEffect 时,Vue 无法自动绑定到组件生命周期:

<script setup>
  let stop;
  onMounted(() => {
    setTimeout(() => {
      // 异步创建:需手动管理
       stop = watchEffect(() => { /* ... */ })   
    }, 1000)
  })
  // 在组件卸载时手动停止
  onUnmounted(()=>{
    stop?.() 
  })
</script>
(2) 临时监听场景

当 watchEffect 仅用于特定阶段的临时监听(如用户操作期间、弹窗显示期间),在阶段结束后必须停止。

let stop;
// 弹窗打开时监听数据变化
function openDialog() {
  const dialogRef = ref(true);
  stop = watchEffect(() => {
    if (store.state.dataUpdated) {
      console.log('数据更新,刷新弹窗');
    }
  });

  // 弹窗关闭时停止监听
  const closeDialog = () => {
    dialogRef.value = false;
    stop?.(); // 必须停止,否则会继续响应数据变化
  };
}
(3)在组件外部使用 watchEffect 时

在组件外部(如工具函数、全局模块、路由守卫等)使用 watchEffect 时,由于没有组件的生命周期自动管理,必须手动停止

// 路由守卫中使用
import { watchEffect } from 'vue';
import { useRouter } from 'vue-router';

const router = useRouter();
let stop;

// 进入路由时启动监听
router.beforeEach((to, from, next) => {
  stop= watchEffect(() => {
    console.log('路由参数变化:', to.params);
  });
  next();
});

// 离开路由时必须停止,否则会污染其他路由
router.afterEach(() => {
  stop?.();
});

二、清理副作用:onCleanup的妙用

想象这样的场景:你用 watchEffect 监听搜索关键词,每次变化都发送请求。但如果前一次请求还没返回,关键词又变了,就会导致请求竞态。这时候,清理副作用的能力就派上用场了。

watchEffect 的回调函数可以接收一个 onCleanup函数,用于注册清理逻辑:

watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log(`搜索:${keyword.value}`)
  }, 1000)
  
  // 注册清理函数:依赖变化或停止监听时执行
  onCleanup(() => {
    clearTimeout(timer)
    console.log('清理定时器')
  })
})

当 watchEffect 重新执行或被停止时,会先调用上一次注册的清理函数,再执行新的副作用。这就像侦察兵每次出发前,都会先清理上一次行动的痕迹。

示例1:关键词搜索避免异步请求竞态

<template>
  <div>
    <input v-model="searchQuery" placeholder="搜索...">
    <div v-if="loading">加载中...</div>
    <ul v-else>
      <li v-for="result in results" :key="result.id">{{ result.name }}</li>
    </ul>
  </div>
</template>

<script setup>
import { ref, watchEffect } from 'vue'

const searchQuery = ref('')
const results = ref([])
const loading = ref(false)

watchEffect((onCleanup) => {
  // 副作用清理函数
  let ignore = false
  
  async function fetchData() {
    if (!searchQuery.value.trim()) {
      results.value = []
      return
    }
    
    loading.value = true
    try {
      const response = await fetch(`/search?keyword=${keyword.value}`)
      const data = await response.json()
      // 如果未忽略,设置结果
      if (!ignore) {
        results.value = data
        loading.value = false
      }
    } catch (error) {
      if (!ignore) {
        console.error('搜索失败:', error)
        loading.value = false
      }
    }
  }
  
  fetchData()
  
  // 清理函数:在下次执行前调用
  onCleanup(() => {
    ignore = true
  })
})
</script>

在这个搜索示例中,我们使用onCleanup来处理异步操作的竞态条件:当用户快速输入时,之前的请求结果可能比新请求更晚返回。通过ignore标志,确保只使用最新的请求结果。清理函数在下次副作用执行前被调用,将ignore设为true。

示例2::集成第三方地图库(如 OpenLayers),需要动态清理资源

import { ref, watchEffect } from 'vue'
import Map from 'ol/Map'
import View from 'ol/View'

const zoomLevel = ref(5)

watchEffect((onCleanup) => {
  const map = new Map({
    target: 'map-container',
    layers: [/* 图层配置 */],
    view: new View({ zoom: zoomLevel.value })
  })
  
  // 清理函数:销毁地图实例和事件监听
  onCleanup(() => {
    map.setTarget(null) // 解除DOM绑定
    map.dispose()       // 释放资源
    console.log('地图实例已销毁')
  })
})

每次创建新地图先销毁上一次地图实例和事件监听,清理资源避免内存泄漏

清理函数的另一种替代写法

在3.5+版本中也可以使用onWatcherCleanup清理副作用

import { onWatcherCleanup } from 'vue'

watchEffect(async () => {
  const { response, cancel } = doAsyncWork(newId)
  // 如果 `id` 变化,则调用 `cancel`,
  // 如果之前的请求未完成,则取消该请求
  onWatcherCleanup(cancel)
  data.value = await response
})

三、控制执行时机:flush 选项的深度应用

类型:

function watchEffect(
  effect: (onCleanup: OnCleanup) => void,
  options?: WatchEffectOptions
): WatchHandle

interface WatchEffectOptions {
  flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
}

第二个参数是一个可选的选项有个flush参数用来设置副作用的执行时机, 默认情况下,watchEffect 会在同步执行期间追踪依赖,并在DOM 更新前执行副作用(flush: ‘pre’)。但有时我们需要调整这个时机。

flush 选项执行时机适用场景
‘pre’(默认)依赖变化后,DOM 更新前读取 DOM 之前的准备工作
‘post’DOM 更新后需要访问更新后的 DOM (如获取元素尺寸)
‘sync’依赖变化时立即执行需要同步响应的场景(谨慎使用,可能影响性能)

示例1:数据更新后,需要获取元素的最新尺寸或位置:

const el = ref(null)
const width = ref(0)

// 用 flush: 'post' 确保能获取到更新后的 DOM 尺寸
watchEffect(() => {
  // 此时 el 已经更新到最新状态
  width.value = el.value?.offsetWidth || 0
}, { flush: 'post' })

示例2 :在显示弹窗后自动聚焦输入框


const showModal = ref(false)

// 默认模式(flush: 'pre')下操作DOM会失败!
watchEffect(() => {
  if (showModal.value) {
    // 此时DOM尚未更新,获取不到元素!
    document.getElementById('email-input')?.focus()
  }
}, {
  flush: 'post' // 确保在DOM更新后执行
})

替代写法小技巧:

可以用 watchPostEffect 和 watchSyncEffect 这两个便捷别名表示 flush: ‘post’ 和 flush: ‘async’ ,让代码更简洁:

import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  // 等价于 flush: 'post'
  width.value = el.value?.offsetWidth || 0
})

四、调试神器:onTrack 与 onTrigger

类型

function watchEffect(
  effect: (onCleanup: OnCleanup) => void,
  options?: WatchEffectOptions
): WatchHandle


interface WatchEffectOptions {
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}

当 watchEffect 出现 “莫名奇妙” 的触发时,不用再抓头发了,第二个参数可选的选项 ——onTrack 和 onTrigger 选项能帮你精准定位问题。

onTrack 选项

  • 触发时机:当响应式依赖被追踪时触发。
  • 发时机:在副作用函数首次执行期间,Vue 追踪到某个响应式属性被访问时

onTrigger 选项

  • 作用:当依赖发生变化,触发副作用函数重新执行前调用。
  • 触发时机:在响应式属性被修改后,但副作用函数执行前。

示例

import { ref, watchEffect } from 'vue';

const count = ref(0);

watchEffect(
  () => {
    console.log('副作用执行:', count.value);
  },
  {
    onTrack(e) {
      console.log('追踪依赖:', e.type, e.target, e.key);
    },
    onTrigger(e) {
      console.log('触发更新:', e.type, e.key, e.oldValue, e.newValue);
    }
  }
);

// 修改 count 值
count.value++;

输出结果:

追踪依赖: get {value: 0} value
副作用执行: 0
触发更新: set value 0 1
副作用执行: 1

注意事项

  • 仅用于调试:onTrack 和 onTrigger 主要用于开发和调试阶段,生产环境中应避免使用。
  • 生产环境自动移除:在生产构建中,Vue 会自动移除这些选项以减少包体积。
  • 性能影响:频繁的追踪和触发回调可能影响性能,仅在必要时使用。

五、依赖规避:让 watchEffect 选择性 “失明”

有时候我们需要在副作用中访问响应式数据,但又不希望它成为依赖。这时可以用一些小技巧让 watchEffect 暂时 “失明”。

示例:日志记录中的依赖规避

在用户操作日志中,需要记录当前登录用户的名称(响应式数据),但日志的触发只应由操作行为本身决定,而非用户信息变化。

import { watchEffect, ref, unref } from 'vue'

// 响应式用户信息(可能会变化,如切换账号)
const currentUser = ref({ name: '张三' })
// 操作行为标记(只有它变化时才该触发日志)
const action = ref('') 
const logs = ref([])

watchEffect(() => {
  if (!action.value) return

  // 关键:用unref获取用户名,不建立依赖
  const userName = unref(currentUser).name
  
  // 只有action变化时才记录,用户信息变化不触发
  logs.value.push(`[${new Date().toLocaleTimeString()}] ${userName} 执行了 ${action.value}`)
})

六、总结

watchEffect 看似简单,但其设计蕴含了 Vue3 响应式系统的精髓。掌握这些进阶特性后,你可以:

  • 用 stop 函数精准控制监听生命周期
  • 用 onCleanup处理副作用清理,避免内存泄漏
  • 用 flush 选项控制执行时机,完美配合 DOM 操作
  • 用 onTrack/onTrigger 快速定位响应式问题
  • 灵活规避不必要的依赖,优化性能

在 Vue3 的响应式生态中,watchEffect 如同智能传感器——自动感知变化,但需要合理布局才能发挥最大价值。掌握这些进阶技巧,让你的代码更加健壮高效!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pixle0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值