一、先看一个真实的痛点假设页面上有三个模块用户信息、文章列表、通知数量。每个都需要从后端拿数据。如果直接用 Axios 写在组件里代码大概长这样vuetemplate div !-- 用户信息 -- div v-ifuserLoading用户信息加载中.../div div v-else-ifuserError用户信息加载失败{{ userError }}/div div v-else{{ user?.name }}/div !-- 文章列表 -- div v-ifarticleLoading文章加载中.../div div v-else-ifarticleError文章加载失败{{ articleError }}/div ul v-elseli v-foritem in articles :keyitem.id{{ item.title }}/li/ul !-- 通知数量 -- div v-ifnotifyLoading通知加载中.../div div v-else-ifnotifyError通知加载失败{{ 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问题很明显每加一个请求就要手动写loading、error、data三个状态。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 } }使用方式vuetemplate div div v-ifuserLoading加载中.../div div v-else-ifuserError错误{{ 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定义loading、error、data了。不需要写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 能“刷新”而不是只赋值一次现在data是ref(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所有还在路上的请求即使返回也不会更新状态避免“组件已销毁但请求还在跑”的报错。六、实战完整的列表页带分页、刷新、错误重试综合上面所有优化做一个完整的文章列表页面。vuetemplate div classarticle-list h2文章列表/h2 !-- 首次加载中 -- div v-ifloading articles.length 0加载中.../div !-- 加载失败 -- div v-else-iferror articles.length 0 p加载失败{{ error }}/p button clickrefresh重试/button /div !-- 文章列表 -- div v-else div v-forarticle in articles :keyarticle.id classarticle-item h3{{ article.title }}/h3 p{{ article.summary }}/p /div !-- 加载更多时的小 loading -- div v-ifloading articles.length 0加载更多.../div !-- 没有更多了 -- p v-ifnoMore—— 到底了 ——/p !-- 加载更多按钮 -- button v-if!noMore clickloadMore加载更多/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这个组合式函数解决了状态管理重复loading、error、data 自动管理。自动执行immediate配置让组件挂载时自动发请求。数据累加accumulate配置让分页数据自动累加。竞态问题请求序号让旧请求结果自动失效。组件卸载安全卸载后自动忽略还在路上的请求。这个useAsync可以直接用到你自己的项目里不管你用的是 Vue3 Axios 还是其他请求库它都不依赖任何具体实现只需要一个返回 Promise 的函数就行。有问题评论区说看到就回。下篇见