Vue 3.4+ 的新特性
1. watch 中的 onCleanup
javascript
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
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
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
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
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
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
ini
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
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
javascript
watch(source, async (value, oldValue, onCleanup) => {
let cancelled = false
// 先设置取消标志
onCleanup(() => {
cancelled = true
})
// 然后执行异步操作
const data = await fetchData(value)
// 操作完成后检查是否被取消
if (!cancelled) {
// 更新状态
}
})
2. 组合使用多种清理
javascript
scss
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
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
scss
// 错误示例:忘记清理
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取消网络请求 - 在组合式函数中始终返回清理函数