数据获取之 useAsyncData
本教程共 50 篇 · 第 23 篇 · 更新于 2026-08-08 · 约 7 分钟阅读
本节目标:搞懂 useAsyncData 和 useFetch 的本质区别,学会用 handler 包装任意异步逻辑来拿数据。
23-1
上一章讲了 useFetch,它最方便,给个 URL 就够了。但你可能也注意到:useFetch 的底子其实是 useAsyncData——准确说,useFetch(url) 几乎就等于 useAsyncData(url, () => $fetch(url))。
一句话区分:
useFetch为你「封装好了怎么发请求」,你只管给 URL。useAsyncData不管你怎么取数据,你给它一个「取数据的函数」就行,这个函数内部爱用$fetch、用第三方 SDK、还是读数据库都随你。
Note当你用的是 CMS、Supabase、Prisma 这类「自带查询层」的工具时,它们往往不希望你直接拼 URL 去
$fetch,而是提供自己的客户端函数。这时候useFetch就不好使了,得用useAsyncData把那个客户端函数包起来。
23-2
useAsyncData 的第一个参数是一个「键」(key),第二个参数是一个「处理函数」(handler,返回 Promise)。它有两种常见写法:
<script setup lang="ts">
// 写法一:显式给一个 key,再给一个处理函数
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))
// 写法二:直接给处理函数,key 由 Nuxt 自动生成
const { data, error } = await useAsyncData(() => myGetFunction('users'))
</script>
那个「键」是用来标识这次请求的,作用很大:
- 缓存依据:相同 key 的多次调用,会共享同一份数据和状态。
- 跨组件共享:两个组件用同一个 key,拿到的就是同一份
data。 - 精准刷新:后面章节会讲,刷新时可以指定 key 只刷某一份数据。
Warning自动生成的 key 只跟你「在哪个文件、哪一行调用」有关。如果你自己封装了一个组合式函数再去包
useAsyncData,不同地方调用会生成不同 key,缓存就乱了。所以自己写封装时,一定要手动给一个明确、唯一的 key。
23-3
handler 就是一个返回 Promise 的普通函数,你想写什么写什么。比如同时并行请求两个接口,再拼成一个结果:
<script setup lang="ts">
const { data: discounts, status } = await useAsyncData('cart-discount', async () => {
const [coupons, offers] = await Promise.all([
$fetch('/api/coupons'),
$fetch('/api/offers'),
])
return { coupons, offers }
})
// 之后用 discounts.value.coupons 和 discounts.value.offers
</script>
再比如,动态 id 的场景:
<script setup lang="ts">
const { id } = useRoute().params
const { data, error } = await useAsyncData(`user:${id}`, () => {
return myGetFunction('users', { id })
})
</script>
23-4
如果你希望「key 随某个响应式值变化、变化了就自动重新请求」,可以用计算属性或 getter 当 key:
<script setup lang="ts">
const userId = ref('123')
const { data: user } = useAsyncData(
computed(() => `user-${userId.value}`),
() => fetchUser(userId.value),
)
// 改 userId,数据会自动重抓,旧数据若没人用会被清理
userId.value = '456'
</script>
Tip当多个组件用同一个 key 调用
useAsyncData或useFetch,它们共享同一份data、error、status。但有个前提:handler 函数、transform、pick、default等「数据相关」的选项必须保持一致,否则开发环境会报警告。而server、lazy、immediate、watch这些「行为相关」的选项可以不同。
23-5
useAsyncData 的定位是「获取并缓存数据」,不是「做事」。一个常见错误是把它拿来调 Pinia 的 action:
<script setup lang="ts">
const offersStore = useOffersStore()
// ❌ 不要这样写
// await useAsyncData(() => offersStore.getOffer(route.params.slug))
</script>
因为 useAsyncData 可能在缓存命中、重新执行等情况下反复跑,把副作用(改状态、发请求)塞进去会导致意想不到的重复执行。如果真要在初始化时做点事,用 callOnce 工具:
<script setup lang="ts">
const offersStore = useOffersStore()
// ✅ 只执行一次,专门用来触发副作用
await callOnce(() => offersStore.getOffer(route.params.slug))
</script>
23-6
handler 拿回来的原始数据,常常不是你想要的模样。useAsyncData 支持 transform 选项,在数据返回后、交给你之前先做一步映射;也支持 default 选项,给「还没拿到数据时」先放一个占位值。
<script setup lang="ts">
const { data: mountains } = await useAsyncData('mountains', () => $fetch('/api/mountains'), {
// 只保留标题和描述,去掉冗余字段
transform: (list) => list.map(m => ({ title: m.title, description: m.description })),
// 请求完成前,data 先是这个空数组,模板不会因 undefined 报错
default: () => [],
})
</script>
这两个选项和 useFetch 是通用的。transform 特别适合「接口字段多、页面只用到一小部分」的场景,既能精简数据,也能顺手改类型。default 则在 lazy 或 server: false 这类「先不取数」的场景下很有用,能避免 data 是 undefined 引发的渲染异常。
Note
transform只改变「你拿到手的数据」,并不会减少服务器实际去取的量;要减少 payload 体积,还得配合pick或干脆在 server 端就只返回需要的字段。两者作用在不同环节,可以叠加使用。
23-7
和 useFetch 一样,useAsyncData 出错时不会抛异常中断渲染,而是把错误装进返回的 error 里。你得在模板里判断:
<script setup lang="ts">
const { data, error, status } = await useAsyncData('users', () => $fetch('/api/users'))
</script>
<template>
<p v-if="error">加载失败:{{ error.message }}</p>
<ul v-else-if="data">
<li v-for="u in data" :key="u.id">{{ u.name }}</li>
</ul>
</template>
如果你希望「出错就直接让 Nuxt 的错误页接管」,也可以在 handler 里手动 throw createError(...)。具体怎么自定义错误页,我们留到后面的错误处理章节细讲。
23-8
前面 useFetch 讲过的返回值,在 useAsyncData 这边一个不少:data、pending/status、error、refresh/execute、clear 全都有。所以两个组合式函数你只要学会一个,另一个几乎不用重新记。
<script setup lang="ts">
const { data, error, refresh } = await useAsyncData('users', () => $fetch('/api/users'))
</script>
<template>
<button @click="refresh()">刷新</button>
</template>
23-9
给你一个简单的判断:
- 数据来自一个 HTTP 接口、你只想给个 URL → 用
useFetch,最省事。 - 你要调用的是某个库自己的查询函数,或要并行组合多个请求、做复杂转换 → 用
useAsyncData。
Note两者共享同一套缓存、同一套返回值、同一套选项(
lazy、server、watch、transform等)。区别只在「谁来决定请求方式」:useFetch内部用$fetch,useAsyncData由你提供 handler。
23-10
useAsyncData 在 Nuxt 3 与 Nuxt 4 中用法一致,无行为差异。若维护 Nuxt 3 老项目,页面文件路径是 pages/xxx.vue 而非 app/pages/xxx.vue,但函数本身照旧。
23-11
useAsyncData 是 useFetch 的「更通用版本」。它不绑定 URL,而是接收一个返回 Promise 的 handler,因此能包住任何异步取数逻辑。理解「key 是缓存与共享的核心」这一点,你就能在组件间优雅地复用数据。下一章我们来聊一个更底层的话题:同样一段取数代码,在服务器和浏览器里到底是怎么跑的,以及为什么会出现「水合不匹配」。
23-7 useAsyncData 与 useFetch 的关系
很多初学者会困惑:既然有了 useFetch,为什么还需要 useAsyncData?简单来说,useFetch 是 useAsyncData 的高层封装。useFetch 内部就是调用了 useAsyncData,同时帮你处理了请求 URL 的拼接和 $fetch 的调用。
当你需要的不只是”请求一个 URL”时,useAsyncData 就派上用场了。比如你想在获取数据前做一些本地计算、调用多个 API 再合并结果、或者使用非 HTTP 的数据源(如本地数据库查询),这些场景都需要 useAsyncData 的灵活性。
选择的原则很简单:如果只是请求一个 API 地址,用 useFetch;如果获取数据的过程涉及额外逻辑,用 useAsyncData。两者在 SSR hydration、缓存、去重等方面的行为完全一致。
23-8 处理复杂的异步数据流
当页面需要从多个 API 获取数据,且这些数据之间有依赖关系时,异步数据流会变得复杂。比如先获取用户信息,再根据用户 ID 获取其订单列表。这种串行依赖的场景,用 async/await 按顺序处理即可。
如果多个数据获取之间没有依赖关系,应该并行执行以节省时间。在 useAsyncData 里,可以用 Promise.all 同时发起多个请求。Nuxt 会等待所有请求完成后,一次性把数据发送给客户端进行 hydration。
对于需要轮询的场景(如实时数据看板),可以结合 setInterval 和 useFetch 的 refresh 方法,定期重新获取数据。记得在组件卸载时清除定时器,避免内存泄漏。