系列文章目录
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 如同智能传感器——自动感知变化,但需要合理布局才能发挥最大价值。掌握这些进阶技巧,让你的代码更加健壮高效!