Vue 3.4+ 的新特性
1.watch中的onCleanup
javascript
import { ref, watch } from 'vue' const searchQuery = ref('') const searchResults = ref([]) // 监听搜索词变化,自动清理前一个请求 watch(searchQuery, async (newValue, oldValue, onCleanup) => { if (!newValue.trim()) return let cancelled = false const controller = new AbortController() // 注册清理函数 onCleanup(() => { cancelled = true controller.abort() }) try { const response = await fetch(`/api/search?q=${newValue}`, { signal: controller.signal }) const data = await response.json() // 检查是否已取消 if (!cancelled) { searchResults.value = data } } catch (error) { if (error.name !== 'AbortError' && !cancelled) { console.error('搜索失败:', error) } } })2.watchEffect中的onCleanup
javascript
import { ref, watchEffect } from 'vue' const userId = ref(1) const userData = ref(null) // watchEffect 自动追踪依赖,包含清理函数 watchEffect(async (onCleanup) => { const id = userId.value let cancelled = false const controller = new AbortController() onCleanup(() => { cancelled = true controller.abort() }) try { const response = await fetch(`/api/users/${id}`, { signal: controller.signal }) const data = await response.json() if (!cancelled) { userData.value = data } } catch (error) { if (error.name !== 'AbortError' && !cancelled) { console.error('获取用户失败:', error) } } })🏗️实际使用场景
场景1:搜索功能(推荐方案)
javascript
import { ref, watch } from 'vue' export function useSearch() { const searchQuery = ref('') const results = ref([]) const isLoading = ref(false) // 防抖函数 const debounce = (fn, delay) => { let timeoutId return (...args) => { clearTimeout(timeoutId) timeoutId = setTimeout(() => fn(...args), delay) } } // 监听搜索词变化 const stopWatch = watch(searchQuery, async (newValue, oldValue, onCleanup) => { if (newValue.trim().length < 2) { results.value = [] return } let cancelled = false const controller = new AbortController() // 注册清理函数 onCleanup(() => { cancelled = true controller.abort() isLoading.value = false }) // 添加延迟防止频繁请求 await new Promise(resolve => setTimeout(resolve, 300)) if (cancelled) return isLoading.value = true try { const response = await fetch(`/api/search?q=${encodeURIComponent(newValue)}`, { signal: controller.signal }) if (cancelled) return const data = await response.json() if (!cancelled) { results.value = data } } catch (error) { if (error.name !== 'AbortError' && !cancelled) { console.error('搜索失败:', error) results.value = [] } } finally { if (!cancelled) { isLoading.value = false } } }) return { searchQuery, results, isLoading, stopWatch } }场景2:轮询数据
javascript
import { ref, watch } from 'vue' export function usePollingData() { const isPolling = ref(false) const data = ref(null) const error = ref(null) watch(isPolling, (shouldPoll, _, onCleanup) => { if (!shouldPoll) { data.value = null return } let cancelled = false let intervalId // 清理函数 onCleanup(() => { cancelled = true if (intervalId) { clearInterval(intervalId) } }) const fetchData = async () => { if (cancelled) return try { const response = await fetch('/api/data') const result = await response.json() if (!cancelled) { data.value = result error.value = null } } catch (err) { if (!cancelled) { error.value = err } } } // 立即获取一次 fetchData() // 设置轮询 intervalId = setInterval(fetchData, 5000) }) return { isPolling, data, error, togglePolling: () => isPolling.value = !isPolling.value } }场景3:多数据源监听
javascript
import { ref, watch } from 'vue' export function useDashboardData() { const filters = ref({ dateRange: 'today', category: 'all' }) const metrics = ref(null) const chartData = ref(null) // 监听多个数据源 watch([() => filters.value.dateRange, () => filters.value.category], async ([dateRange, category], _, onCleanup) => { let cancelled = false const controller = new AbortController() onCleanup(() => { cancelled = true controller.abort() }) // 并行请求多个数据 try { const [metricsRes, chartRes] = await Promise.all([ fetch(`/api/metrics?range=${dateRange}&category=${category}`, { signal: controller.signal }), fetch(`/api/chart-data?range=${dateRange}&category=${category}`, { signal: controller.signal }) ]) if (cancelled) return const [metricsData, chartDataResult] = await Promise.all([ metricsRes.json(), chartRes.json() ]) if (!cancelled) { metrics.value = metricsData chartData.value = chartDataResult } } catch (error) { if (error.name !== 'AbortError' && !cancelled) { console.error('获取数据失败:', error) } } }, { immediate: true }) return { filters, metrics, chartData } }🔄组合式函数封装
高级封装:useAsyncWatch
javascript
import { ref, watch, onUnmounted } from 'vue' export function useAsyncWatch(source, asyncFn, options = {}) { const { immediate = false, debounce = 0, deep = false } = options const data = ref(null) const error = ref(null) const isLoading = ref(false) let cleanupFn = null // 停止监听函数 const stop = watch(source, async (newValue, oldValue, onCleanup) => { let cancelled = false // 如果有防抖需求 if (debounce > 0) { await new Promise(resolve => setTimeout(resolve, debounce)) if (cancelled) return } isLoading.value = true error.value = null // 注册当前清理函数 onCleanup(() => { cancelled = true isLoading.value = false }) // 保存清理函数供外部调用 cleanupFn = () => { cancelled = true isLoading.value = false } try { const result = await asyncFn(newValue, oldValue, () => cancelled) if (!cancelled) { data.value = result } } catch (err) { if (!cancelled) { error.value = err } } finally { if (!cancelled) { isLoading.value = false } } }, { immediate, deep }) // 手动取消当前操作 const cancel = () => { if (cleanupFn) { cleanupFn() cleanupFn = null } } // 重新触发 const trigger = () => { const currentValue = typeof source === 'function' ? source() : source.value // 这里需要手动触发 watch 回调 cancel() // 可以结合 options.immediate 或重新设置值 } onUnmounted(() => { stop() cancel() }) return { data, error, isLoading, cancel, trigger, stop } } // 使用示例 const searchQuery = ref('') const { data: results, isLoading, cancel } = useAsyncWatch( searchQuery, async (query, oldValue, isCancelled) => { if (!query.trim() || isCancelled()) return null const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 10000) try { const response = await fetch(`/api/search?q=${query}`, { signal: controller.signal }) if (isCancelled()) return null return await response.json() } finally { clearTimeout(timeoutId) } }, { debounce: 300, immediate: false } )处理竞态的通用 Hook
javascript
export function useRaceConditionWatch(source, asyncFn, options = {}) { const { immediate = false, cancelPrevious = true } = options const data = ref(null) const error = ref(null) const isLoading = ref(false) let currentToken = null const stop = watch(source, async (newValue, oldValue, onCleanup) => { const token = Symbol('request') currentToken = token let cancelled = false let abortController = null onCleanup(() => { cancelled = true if (abortController) { abortController.abort() } if (currentToken === token) { isLoading.value = false } }) if (cancelPrevious && currentToken !== token) { return // 已经有新的请求 } isLoading.value = true error.value = null try { abortController = new AbortController() const result = await asyncFn(newValue, abortController.signal, () => cancelled) // 检查是否是当前最新请求 if (!cancelled && currentToken === token) { data.value = result } } catch (err) { if (err.name !== 'AbortError' && !cancelled && currentToken === token) { error.value = err } } finally { if (!cancelled && currentToken === token) { isLoading.value = false } } }, { immediate }) return { data, error, isLoading, stop } }🎯实际案例:实时聊天
javascript
import { ref, watch, onUnmounted } from 'vue' export function useChatRoom(roomId) { const messages = ref([]) const isConnected = ref(false) let socket = null let reconnectTimer = null // 监听 roomId 变化 watch(() => roomId.value, (newRoomId, oldRoomId, onCleanup) => { if (!newRoomId) { messages.value = [] isConnected.value = false return } let cancelled = false onCleanup(() => { cancelled = true // 清理 WebSocket 连接 if (socket) { socket.close() socket = null } // 清理重连定时器 if (reconnectTimer) { clearTimeout(reconnectTimer) reconnectTimer = null } }) const connectWebSocket = () => { if (cancelled) return socket = new WebSocket(`wss://api.example.com/chat/${newRoomId}`) socket.onopen = () => { if (!cancelled) { isConnected.value = true } } socket.onmessage = (event) => { if (!cancelled) { const message = JSON.parse(event.data) messages.value.push(message) } } socket.onclose = () => { if (!cancelled) { isConnected.value = false // 尝试重连 if (!cancelled) { reconnectTimer = setTimeout(connectWebSocket, 3000) } } } socket.onerror = (error) => { if (!cancelled) { console.error('WebSocket 错误:', error) } } } connectWebSocket() }, { immediate: true }) const sendMessage = (content) => { if (socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ content })) } } onUnmounted(() => { if (socket) { socket.close() } }) return { messages, isConnected, sendMessage } }📝最佳实践
1.正确的清理顺序
javascript
watch(source, async (value, oldValue, onCleanup) => { let cancelled = false // 先设置取消标志 onCleanup(() => { cancelled = true }) // 然后执行异步操作 const data = await fetchData(value) // 操作完成后检查是否被取消 if (!cancelled) { // 更新状态 } })2.组合使用多种清理
javascript
watch(source, async (value, oldValue, onCleanup) => { let cancelled = false const controller = new AbortController() const timeoutId = setTimeout(() => { controller.abort() }, 10000) // 注册多个清理操作 onCleanup(() => { cancelled = true controller.abort() clearTimeout(timeoutId) }) // 异步操作... })3.处理竞态条件的模式
javascript
const useLatestRequest = (asyncFn) => { let currentRequest = null return async (...args) => { // 取消前一个请求 if (currentRequest?.cancel) { currentRequest.cancel() } const controller = new AbortController() const request = { promise: asyncFn(...args, controller.signal), cancel: () => controller.abort() } currentRequest = request try { const result = await request.promise // 检查是否仍然是当前请求 if (currentRequest === request) { return result } return null } catch (error) { if (error.name !== 'AbortError') { throw error } return null } } }4.避免的内存泄漏
javascript
// 错误示例:忘记清理 watch(source, async () => { const timer = setInterval(() => { // 做一些事情 }, 1000) // 忘记清理定时器! }) // 正确示例:使用 onCleanup watch(source, async (value, oldValue, onCleanup) => { const timer = setInterval(() => { // 做一些事情 }, 1000) onCleanup(() => { clearInterval(timer) }) })🚀总结onCleanup的核心优势:
- 自动清理:watch 回调执行前自动调用清理函数
- 竞态安全:确保只有最后一次请求的结果被处理
- 内存安全:防止内存泄漏
- 简化代码:无需手动管理清理逻辑
使用建议:
- 所有涉及异步操作的 watch 都应该使用
onCleanup - 对于定时器、WebSocket、订阅等资源,必须使用清理函数
- 结合
AbortController取消网络请求 - 在组合式函数中始终返回清理函数
原文: https://juejin.cn/post/75925591