当前位置: 首页 > news >正文

Vue3 异步数据管理:从满地都是 loading 到优雅的 useRequest,保姆级优化之路

一、先看一个真实的痛点

假设页面上有三个模块:用户信息、文章列表、通知数量。每个都需要从后端拿数据。

如果直接用 Axios 写在组件里,代码大概长这样:

vue

<template> <div> <!-- 用户信息 --> <div v-if="userLoading">用户信息加载中...</div> <div v-else-if="userError">用户信息加载失败:{{ userError }}</div> <div v-else>{{ user?.name }}</div> <!-- 文章列表 --> <div v-if="articleLoading">文章加载中...</div> <div v-else-if="articleError">文章加载失败:{{ articleError }}</div> <ul v-else><li v-for="item in articles" :key="item.id">{{ item.title }}</li></ul> <!-- 通知数量 --> <div v-if="notifyLoading">通知加载中...</div> <div v-else-if="notifyError">通知加载失败:{{ notifyError }}</div> <div v-else>{{ notifyCount }} 条新通知</div> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { getUserInfo, getArticles, getNotifyCount } from '@/api' // 用户信息相关状态 const user = ref(null) const userLoading = ref(false) const userError = ref(null) // 文章列表相关状态 const articles = ref([]) const articleLoading = ref(false) const articleError = ref(null) // 通知数量相关状态 const notifyCount = ref(0) const notifyLoading = ref(false) const notifyError = ref(null) // 获取用户信息 async function fetchUser() { userLoading.value = true userError.value = null try { user.value = await getUserInfo() } catch (e) { userError.value = e.message } finally { userLoading.value = false } } // 获取文章列表 async function fetchArticles() { articleLoading.value = true articleError.value = null try { articles.value = await getArticles() } catch (e) { articleError.value = e.message } finally { articleLoading.value = false } } // 获取通知数量 async function fetchNotify() { notifyLoading.value = true notifyError.value = null try { notifyCount.value = await getNotifyCount() } catch (e) { notifyError.value = e.message } finally { notifyLoading.value = false } } onMounted(() => { fetchUser() fetchArticles() fetchNotify() }) </script>

问题很明显:

  • 每加一个请求,就要手动写loadingerrordata三个状态。

  • try/catch/finally模板代码复制来复制去,整个组件一大半是重复逻辑。

  • 三个请求各管各的,互相之间不好协调。

目标:把这些重复的“状态管理”逻辑抽出去,组件只关心“拿什么数据”和“怎么展示”。


二、第一步:写一个最简单的 useAsync

把“发请求 + 管状态”抽成一个组合式函数。

javascript

// src/hooks/useAsync.js import { ref } from 'vue' // asyncFn 是一个返回 Promise 的函数,比如 () => getUserInfo() export function useAsync(asyncFn) { const data = ref(null) // 存放请求返回的数据 const loading = ref(false) // 是否正在加载 const error = ref(null) // 错误信息 // 执行请求的函数 async function execute(...args) { // 每次执行前重置状态 loading.value = true error.value = null // 注意:data 不在这里清空,因为有时候想让旧数据先保留着 // 如果想清空,加一行 data.value = null try { // 调用传进来的异步函数,把参数原样传过去 const result = await asyncFn(...args) data.value = result // 成功就把结果存起来 return result // 把结果也返回出去,方便外面链式调用 } catch (err) { // 失败就把错误信息存起来 error.value = err.message || '请求失败' throw err // 把错误继续抛出,让外面也能 catch } finally { loading.value = false // 不管成功失败,最终 loading 都设成 false } } // 把状态和方法返回出去 return { data, loading, error, execute } }

使用方式:

vue

<template> <div> <div v-if="userLoading">加载中...</div> <div v-else-if="userError">错误:{{ userError }}</div> <div v-else>{{ userData?.name }}</div> </div> </template> <script setup> import { onMounted } from 'vue' import { getUserInfo } from '@/api/user' import { useAsync } from '@/hooks/useAsync' // 把 getUserInfo 函数传进去,拿到状态和 execute 方法 const { data: userData, loading: userLoading, error: userError, execute: fetchUser } = useAsync(getUserInfo) onMounted(() => { fetchUser() }) </script>

对比原始写法:

  • 不需要手动写ref定义loadingerrordata了。

  • 不需要写try/catch/finally了。

  • 组件代码直接从十几行缩减到三行。

useAsync还是有个小问题:每次都要自己调execute。能不能让它在组件挂载时自动执行?


三、第二步:加上自动执行

useAsync加个配置项,让它能选择“自动执行”还是“手动执行”。

javascript

// src/hooks/useAsync.js(升级版) import { ref, onMounted } from 'vue' export function useAsync(asyncFn, options = {}) { const { immediate = false } = options // immediate 为 true 时自动执行 const data = ref(null) const loading = ref(false) const error = ref(null) async function execute(...args) { loading.value = true error.value = null try { const result = await asyncFn(...args) data.value = result return result } catch (err) { error.value = err.message || '请求失败' throw err } finally { loading.value = false } } // 如果 immediate 为 true,在组件挂载时自动执行一次 onMounted(() => { if (immediate) { execute() } }) return { data, loading, error, execute } }

使用:

javascript

// 自动执行:组件挂载时自动发请求 const { data, loading, error } = useAsync(getUserInfo, { immediate: true }) // 手动执行:需要点击按钮之类才发请求 const { data, loading, error, execute } = useAsync(submitForm) // 调用 execute(data) 才发请求

四、第三步:让 data 能“刷新”而不是只赋值一次

现在dataref(null),每次请求成功就覆盖旧值。但有些场景需要追加数据,比如分页加载:新数据应该拼在旧数组后面,而不是替换。

加一个accumulate配置,允许累加数组。

javascript

// src/hooks/useAsync.js(继续升级) import { ref, onMounted } from 'vue' export function useAsync(asyncFn, options = {}) { const { immediate = false, accumulate = false } = options // 如果 accumulate 为 true,数据初始化为空数组 const data = ref(accumulate ? [] : null) const loading = ref(false) const error = ref(null) async function execute(...args) { loading.value = true error.value = null try { const result = await asyncFn(...args) if (accumulate && Array.isArray(result)) { // 累加模式:把新数据追加到旧数组后面 data.value = [...data.value, ...result] } else { data.value = result } return result } catch (err) { error.value = err.message || '请求失败' throw err } finally { loading.value = false } } onMounted(() => { if (immediate) execute() }) return { data, loading, error, execute } }

使用场景:

javascript

// 场景一:普通获取,每次覆盖(比如用户信息) const { data: user, loading } = useAsync(getUserInfo, { immediate: true }) // 场景二:分页加载,每次追加(比如文章列表下拉加载更多) const { data: articles, loading, execute: loadMore } = useAsync( (page) => getArticles({ page }), { accumulate: true } ) // 第一次加载第一页 loadMore(1) // 下拉后加载第二页,会拼到第一页后面 loadMore(2)

五、第四步:处理竞态问题

如果用户快速切换页面,或者连续点按钮,可能会发出多个请求,后发的请求先返回,导致数据错乱。

比如:切换用户 ID 时,先请求用户1(慢),再请求用户2(快),用户2的数据先回来了,但紧接着用户1的数据也回来了,页面最终显示的是旧数据。

我们给useAsync加上请求序号来解决。

javascript

// src/hooks/useAsync.js(完整版) import { ref, onMounted, onBeforeUnmount } from 'vue' export function useAsync(asyncFn, options = {}) { const { immediate = false, accumulate = false } = options const data = ref(accumulate ? [] : null) const loading = ref(false) const error = ref(null) // 记录请求的序号,解决竞态问题 let requestId = 0 async function execute(...args) { // 每次执行前,序号加 1,并记住当前序号 requestId++ const currentRequestId = requestId loading.value = true error.value = null try { const result = await asyncFn(...args) // 请求回来后,检查序号是否还是最新的 // 如果不是最新,说明又发了新请求,这个旧结果直接丢弃 if (currentRequestId !== requestId) { console.log('请求已过期,丢弃结果') return result } if (accumulate && Array.isArray(result)) { data.value = [...data.value, ...result] } else { data.value = result } return result } catch (err) { // 错误也需要判断序号,避免旧请求的错误覆盖新请求的状态 if (currentRequestId === requestId) { error.value = err.message || '请求失败' } throw err } finally { // 同样,只有最新请求才把 loading 置为 false if (currentRequestId === requestId) { loading.value = false } } } onMounted(() => { if (immediate) execute() }) // 组件卸载时,把序号设为负数,让所有进行中的请求都失效 onBeforeUnmount(() => { requestId = -1 }) return { data, loading, error, execute } }

解释:

  • 每次调用execute,内部requestId自增。

  • 请求返回时,检查currentRequestId是否还等于最新的requestId,如果不等说明中间有更新的请求,旧结果被抛弃。

  • 组件卸载时把requestId设为 -1,所有还在路上的请求即使返回也不会更新状态,避免“组件已销毁但请求还在跑”的报错。


六、实战:完整的列表页(带分页、刷新、错误重试)

综合上面所有优化,做一个完整的文章列表页面。

vue

<template> <div class="article-list"> <h2>文章列表</h2> <!-- 首次加载中 --> <div v-if="loading && articles.length === 0">加载中...</div> <!-- 加载失败 --> <div v-else-if="error && articles.length === 0"> <p>加载失败:{{ error }}</p> <button @click="refresh">重试</button> </div> <!-- 文章列表 --> <div v-else> <div v-for="article in articles" :key="article.id" class="article-item"> <h3>{{ article.title }}</h3> <p>{{ article.summary }}</p> </div> <!-- 加载更多时的小 loading --> <div v-if="loading && articles.length > 0">加载更多...</div> <!-- 没有更多了 --> <p v-if="noMore">—— 到底了 ——</p> <!-- 加载更多按钮 --> <button v-if="!noMore" @click="loadMore">加载更多</button> </div> </div> </template> <script setup> import { ref, computed } from 'vue' import { getArticles } from '@/api/article' import { useAsync } from '@/hooks/useAsync' const page = ref(1) const pageSize = 10 // 用 useAsync 管理请求,accumulate: true 让分页数据累加 const { data: articles, loading, error, execute } = useAsync( () => getArticles({ page: page.value, pageSize }), { accumulate: true, immediate: true } ) // 是否已到底 const noMore = computed(() => articles.value.length > 0 && articles.value.length % pageSize !== 0) // 加载更多 function loadMore() { page.value++ execute() } // 刷新(重置页码,清空旧数据) function refresh() { page.value = 1 articles.value = [] // 清空旧数据 execute() } </script>

效果:

  • 进来自动加载第一页。

  • 点“加载更多”追加数据。

  • 失败了可以点“重试”。

  • 完全不用手动写 loading/error 的样板代码。


七、总结

今天我们从一个很乱的组件开始,一步步抽象出了useAsync这个组合式函数,解决了:

  1. 状态管理重复:loading、error、data 自动管理。

  2. 自动执行immediate配置让组件挂载时自动发请求。

  3. 数据累加accumulate配置让分页数据自动累加。

  4. 竞态问题:请求序号让旧请求结果自动失效。

  5. 组件卸载安全:卸载后自动忽略还在路上的请求。

这个useAsync可以直接用到你自己的项目里,不管你用的是 Vue3 + Axios 还是其他请求库,它都不依赖任何具体实现,只需要一个返回 Promise 的函数就行。

有问题评论区说,看到就回。下篇见!

http://www.jsqmd.com/news/1013437/

相关文章:

  • 《鸿蒙原生应用开发实战》第二篇:ArkTS 数据模型与状态管理
  • Notepad--:跨平台文本编辑器的国产之光,打造高效开发新体验
  • SillyTavern终极性能优化实战:从卡顿到流畅的完整指南
  • 5分钟从零制作专业视频:Auto-Video-Generator完全指南
  • (GR-RL)技术密档701-1000号摘要: 本技术文档集聚焦工业级具身智能系统的底层参数与核心算法,涵盖硬件控制、传感融合、运动规划及分布式训练等关键技术指标。主要内容包括:总线仲裁采用伺服驱动优
  • 2026年昆山家电维修机构TOP5盘点 全维度实测对比 - 互联网科技品牌测评
  • 爱回收报价透明吗?三类闲置实测后的判断 - 新闻快传
  • Bugku CTF 神秘的文件
  • MPC7450 MPX总线地址传输机制与缓存一致性实战解析
  • Hitboxer终极指南:免费开源的SOCD键盘重映射工具,彻底解决游戏方向键冲突
  • LaTeX参考文献样式选哪个?8种bibliographystyle(plain/ieeetr/acm...)的详细对比与选择指南
  • 喜报!itc保伦股份荣获第十一届广东专利优秀奖,创新成果再获权威认可 - 品牌速递
  • 国产跨平台文本编辑器终极指南:notepad--如何成为你的高效编程伙伴
  • 爱回收质检透明吗?拆完5道工序我有了判断 - 新闻快传
  • LiteDB.Studio:嵌入式NoSQL数据库的终极可视化管理方案
  • Python量化交易终极指南:Backtrader快速入门与实战教程
  • Ryujinx Switch模拟器完整教程:从零开始快速搭建高性能游戏环境
  • Ryujinx Switch模拟器终极指南:在PC上畅玩任天堂游戏的完整教程
  • 2026年昆山家电故障维修服务商推荐 附选型标准与避坑要点 - 互联网科技品牌测评
  • 杭州闲置黄金怎么卖不亏?2026黄金回收完整避坑攻略,正规门店这样选 - 薛定谔的梨花猫
  • 别再傻傻用ManualResetEvent了!C#高并发场景下,试试这个性能更强的轻量级替代品
  • 终极分屏游戏方案:用Nucleus Co-Op免费开启本地多人游戏新时代
  • 如何在5分钟内用Dify工作流库打造你的专属AI助手?终极解决方案揭秘
  • AI 驱动的前端设计系统生成:从设计令牌到组件库的自动化实践
  • 固定数组时间轮的槽过载优化:桶链表与批次执行
  • OCLP-Mod:如何让2008年后的旧款Mac继续运行最新macOS系统?
  • GR3-Fourier V10.3~V10.9版本的底层驱动算法源码和工业硬件参数标定数据。算法部分涵盖Park变换、斜坡限幅、定时器配置等10个核心功能模块(1-25号)。硬件参数部分详细列出了26
  • MPC8260并行I/O端口配置:引脚复用、中断与UTOPIA/TDM实战
  • GR3六轴工业协作机械臂底层技术档案揭示了35项关键系统设计,涵盖安全保护、运动控制、通讯优化等核心模块。其多重故障保护机制实现毫秒级响应,包括电流异常连锁保护、通讯中断应急处理及分级散热策略。伺服系
  • 终极MTK设备底层调试与刷机完全指南