Vue 3 异步请求控制:串行与并行的优雅实践

在 Vue 3 的开发中,处理异步请求是家常便饭:需要等待一个或多个 API 请求完成后,才能进行下一步的数据处理或页面渲染。如何清晰、高效地控制这些请求的执行顺序,是构建健壮应用的关键。本文结合 async/awaitPromise.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 保证了顺序:第二步的请求一定发生在第一步完成之后;
  • 注意:上面的写法中 fetchUserDetailfetchOrders 虽然写在一行,但仍然是串行的(先等 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 使用

异步控制的本质就四个字:清楚依赖。有依赖就串行,没依赖就并行,再加一层竞态防护,你的页面在任何交互速度下都能保持正确与流畅。