在 Vue 3 的开发中,处理异步请求是家常便饭:需要等待一个或多个 API 请求完成后,才能进行下一步的数据处理或页面渲染。如何清晰、高效地控制这些请求的执行顺序,是构建健壮应用的关键。本文结合 async/await、Promise.all 等现代 JavaScript 特性,系统梳理串行执行与并行执行两种核心模式,并给出可复用的封装。
一、串行执行:处理请求依赖
当一个请求的执行依赖于前一个请求的返回结果时,我们必须确保它们按顺序执行。例如:先获取用户 ID,再根据 ID 获取用户的详细信息和订单列表。
<script setup>
import { ref } from 'vue'
const loading = ref(false)
const userDetail = ref(null)
const orderList = ref([])
async function loadUserData() {
loading.value = true
try {
// 第一步:获取用户 ID
const { id } = await fetchUser()
// 第二步:依赖上一步结果,串行请求
const [detail, orders] = [
await fetchUserDetail(id),
await fetchOrders(id)
]
userDetail.value = detail
orderList.value = orders
} finally {
loading.value = false
}
}
</script>
这里有两个关键点:
- await 保证了顺序:第二步的请求一定发生在第一步完成之后;
- 注意:上面的写法中
fetchUserDetail和fetchOrders虽然写在一行,但仍然是串行的(先等 detail 完成再发 orders 请求)。如果两者相互独立,应当用并行方案。
二、并行执行:互不依赖的请求
当多个请求相互独立时,串行会白白浪费等待时间。比如首页需要同时拉取用户信息、公告列表和统计数据,用 Promise.all 让它们并发执行:
<script setup>
import { ref } from 'vue'
const loading = ref(false)
const profile = ref(null)
const notices = ref([])
const stats = ref(null)
async function loadDashboard() {
loading.value = true
try {
// 三个请求并发发出,总耗时 ≈ 最慢的那个
const [p, n, s] = await Promise.all([
fetchProfile(),
fetchNotices(),
fetchStats()
])
profile.value = p
notices.value = n
stats.value = s
} finally {
loading.value = false
}
}
</script>
2.1 使用 Promise.allSettled 更稳
Promise.all 是"一损俱损":任何一个请求失败,整体就 reject,其他成功的请求结果也拿不到。如果希望"各自为政",用 Promise.allSettled:
const results = await Promise.allSettled([
fetchProfile(),
fetchNotices(),
fetchStats()
])
// results 中每一项都有 status: 'fulfilled' | 'rejected'
const okProfile = results[0].status === 'fulfilled' ? results[0].value : null
三、组合式封装:useAsync
把加载状态、错误处理、竞态防护统一收口,避免每个页面重复样板代码。我们可以封装一个通用的组合式函数:
// composables/useAsync.js
import { ref, readonly } from 'vue'
export function useAsync(fn, { immediate = false } = {}) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
// 竞态防护:只有最新一次调用能更新状态
let seq = 0
async function run(...args) {
const cur = ++seq
loading.value = true
error.value = null
try {
const res = await fn(...args)
if (cur === seq) data.value = res
return res
} catch (e) {
if (cur === seq) error.value = e
throw e
} finally {
if (cur === seq) loading.value = false
}
}
if (immediate) run()
return { data: readonly(data), loading: readonly(loading), error: readonly(error), run }
}
在组件中使用:
<script setup>
import { useAsync } from '../composables/useAsync'
const { data, loading, run } = useAsync(fetchOrders, { immediate: true })
// 下拉刷新时重新请求
async function onRefresh() {
await run()
}
</script>
这个封装解决了三个高频痛点:loading 状态管理、错误捕获、竞态问题(快速连续触发时,只认最后一次结果,避免旧数据覆盖新数据)。
四、实战场景:搜索防抖 + 取消过期请求
搜索框是竞态问题的重灾区:用户快速输入时,前一个请求可能比后一个更晚返回,导致显示旧结果。组合方案:
<script setup>
import { ref, watch } from 'vue'
import { useAsync } from '../composables/useAsync'
const keyword = ref('')
const { data, loading, run } = useAsync(searchByKeyword)
// 500ms 防抖 + 竞态防护(useAsync 内部已处理)
let timer
watch(keyword, () => {
clearTimeout(timer)
timer = setTimeout(() => run(keyword.value), 500)
})
</script>
配合 AbortController 可以进一步取消真正废弃的网络请求:
let controller
async function search(kw) {
controller?.abort() // 取消上一个请求
controller = new AbortController()
return fetch(`/api/search?q=${kw}`, { signal: controller.signal })
.then(r => r.json())
}
五、总结
| 场景 | 推荐方案 | 要点 |
|---|---|---|
| 请求之间有依赖 | async/await 串行 | 后一步 await 前一步结果 |
| 请求相互独立 | Promise.all | 总耗时取最慢请求 |
| 部分失败仍要展示 | Promise.allSettled | 逐项判断 status |
| 高频触发(搜索/刷新) | 防抖 + useAsync | 防抖 + 竞态防护 |
| 需要真正取消请求 | AbortController | 配合 fetch/axios 使用 |
异步控制的本质就四个字:清楚依赖。有依赖就串行,没依赖就并行,再加一层竞态防护,你的页面在任何交互速度下都能保持正确与流畅。