news 2026/5/1 7:05:53

使用 onCleanup处理异步副作用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 onCleanup处理异步副作用

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的核心优势:

  1. 自动清理:watch 回调执行前自动调用清理函数
  2. 竞态安全:确保只有最后一次请求的结果被处理
  3. 内存安全:防止内存泄漏
  4. 简化代码:无需手动管理清理逻辑

使用建议:

  • 所有涉及异步操作的 watch 都应该使用onCleanup
  • 对于定时器、WebSocket、订阅等资源,必须使用清理函数
  • 结合AbortController取消网络请求
  • 在组合式函数中始终返回清理函数



原文: https://juejin.cn/post/75925591
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 5:04:35

跨平台AI绘画:阿里通义Z-Image-Turbo云端解决方案

跨平台AI绘画&#xff1a;阿里通义Z-Image-Turbo云端解决方案实践指南 为什么选择云端AI绘画方案&#xff1f; 最近我在尝试AI绘画时遇到了一个痛点&#xff1a;在不同设备间切换时&#xff0c;环境配置和模型同步总是很麻烦。本地部署不仅需要折腾CUDA环境&#xff0c;还要处理…

作者头像 李华
网站建设 2026/5/1 5:04:52

AI+电商实战:用预置镜像搭建商品主图生成系统

AI电商实战&#xff1a;用预置镜像搭建商品主图生成系统 电商代运营公司每天需要处理上百个商品上架需求&#xff0c;摄影师和修图师资源紧张。他们计划用AI自动生成符合各平台规范的主图&#xff0c;但担心技术门槛过高难以实施。本文将介绍如何利用预置镜像快速搭建商品主图…

作者头像 李华
网站建设 2026/5/1 5:05:23

申请调试Profile

前提条件 已创建HarmonyOS应用 | 创建元服务。 已申请调试证书&#xff0c;并注册调试设备。 &#xff08;如需使用ACL权限&#xff09;已申请并获取ACL权限。 操作步骤 1.登录AppGallery Connect&#xff0c;选择“证书、APP ID和Profile”。 2.在左侧导航栏选择“证书、APP I…

作者头像 李华
网站建设 2026/5/1 5:06:35

技术作家亲测:3种最快捷的AI图像生成环境搭建方案对比

技术作家亲测&#xff1a;3种最快捷的AI图像生成环境搭建方案对比 作为一名长期关注AI技术的写作者&#xff0c;我经常需要为读者评测不同AI工具的部署方案。最让我头疼的就是反复配置环境——每次测试新模型都要从头安装依赖、调试CUDA版本、解决库冲突。直到最近尝试了三种标…

作者头像 李华
网站建设 2026/5/1 5:06:47

Z-Image-Turbo批量处理:自动化生成上千张商业用图

Z-Image-Turbo批量处理&#xff1a;自动化生成上千张商业用图 电商平台常面临商品场景图制作的高成本问题。人工绘制数千张图片不仅耗时费力&#xff0c;风格一致性也难以保证。Z-Image-Turbo正是为解决这一痛点而设计的批量图像生成工具&#xff0c;基于Stable Diffusion技术实…

作者头像 李华
网站建设 2026/4/10 18:25:38

阿里通义Z-Image-Turbo社区版:快速搭建协作开发平台

阿里通义Z-Image-Turbo社区版&#xff1a;快速搭建协作开发平台 为什么需要统一的开发环境 开源社区在协作开发Z-Image-Turbo衍生项目时&#xff0c;经常遇到开发环境不一致导致的兼容性问题。不同成员的CUDA版本、Python依赖、系统配置差异&#xff0c;使得代码在本地运行结果…

作者头像 李华