Skip to content

useAsyncData

useAsyncDatauseFetch 的底层组合式函数,提供了更灵活的异步数据获取方式。理解 useAsyncData 能帮你理解 Nuxt 数据获取的完整机制,也能在 useFetch 不够灵活时提供替代方案。

什么时候用 useAsyncData 而不是 useFetch?

场景用 useFetch用 useAsyncData
标准 API 请求也能用,但更繁琐
聚合多个 API❌ 一个 useFetch 只能请求一个 URL✅ 在获取函数中发多个请求
使用第三方 SDK(Supabase、Firebase)✅ 包裹 SDK 的获取方法
需要自定义获取逻辑✅ 获取函数完全自定义
需要复杂的条件判断有限的 immediate + watch✅ 获取函数内任意逻辑
调用 Nuxt 内部 API✅ 如 queryContent()

简单记忆

useFetch = useAsyncData + $fetch。90% 的情况用 useFetch 就够了。只有在需要更灵活的获取逻辑时才用 useAsyncData

基本用法

ts
const { data, status, error, refresh, clear } = await useAsyncData(
  'users',  // key(必须手动指定)
  () => $fetch('/api/users')  // 获取函数
)

useFetch 的三个关键区别

  1. 必须手动指定 keyuseFetch 自动根据 URL 生成)
  2. 必须手动提供获取函数useFetch 自动用 $fetch
  3. 获取函数可以是任何异步操作(不限于 HTTP 请求)

返回值

useFetch 完全一致:

属性类型说明
dataRef<T | undefined>响应数据
statusRef<string>状态:idle/pending/success/error
errorRef<Error | undefined>错误信息
pendingRef<boolean>是否正在加载
refresh(opts?) => Promise<void>刷新数据
execute(opts?) => Promise<void>refresh 的别名
clear() => void清除数据

refreshexecute 的区别

没有区别,完全相同。execute 这个名字在 immediate: false 场景下语义更清晰——"执行"请求。

key 的重要性与最佳实践

key 是 useAsyncData 最关键的概念,它决定了数据的去重、缓存、SSR 传递

text
key 的三大作用:
├── 1. 去重:多个组件使用相同 key → 共享数据,不重复请求
├── 2. SSR 传递:服务端获取的数据通过 key 存入 Payload
└── 3. 缓存:通过 key 访问已缓存的数据

key 的命名规则

ts
// ✅ 好的 key:语义清晰、唯一
'useAsyncData'('app-config', ...)
'useAsyncData'('dashboard-stats', ...)
'useAsyncData'('user-profile', ...)

// ❌ 坏的 key:太宽泛,可能冲突
'useAsyncData'('data', ...)
'useAsyncData'('list', ...)

动态 key 的正确用法

ts
// ✅ 动态 key:包含参数信息,确保不同参数不共享缓存
const { data } = await useAsyncData(
  `users-page-${page.value}`,  // page 变化时生成新 key
  () => $fetch('/api/users', { query: { page: page.value } }),
  { watch: [page] }
)

// ❌ 完全静态的 key:不同参数共享缓存,导致数据错乱
const { data } = await useAsyncData(
  'users',  // 永远是同一个 key
  () => $fetch('/api/users', { query: { page: page.value } }),
  { watch: [page] }
)

INFO

动态 key 的陷阱:key 变化时 useAsyncData 会创建新的数据实例,旧数据被丢弃。如果你在 watch 中监听了参数变化,同时 key 也应该包含这些参数,否则会出现 key 不变但数据变了的不一致状态

最佳实践

key 中包含所有影响数据的变量。如果数据依赖 pagekeyword,key 应该是 'users-' + page.value + '-' + keyword.value

SSR 中的执行时序

理解 useAsyncData 在 SSR 和客户端的行为差异,是避免 bug 的关键:

首次访问(SSR)

text
1. 用户请求 /dashboard

2. 服务端执行 useAsyncData 获取函数

3. 数据通过 key 存入 nuxtApp.payload.data

4. 服务端用数据渲染 HTML

5. HTML + Payload 一起发送到浏览器

6. 客户端 Hydration 时,从 Payload 读取数据(不重新请求)

客户端导航(页面跳转)

text
1. 用户点击 <NuxtLink to="/settings">

2. 客户端执行 useAsyncData 获取函数

3. 数据在客户端获取(发 HTTP 请求)

4. 页面更新

关键理解

  • SSR 首次加载:数据在服务端获取,客户端直接使用 Payload,不重复请求
  • 客户端导航:数据在客户端获取,发起新请求
  • useAsyncData 自动处理这两种情况,你不需要写条件判断

server: false 时的行为

ts
const { data } = await useAsyncData('client-only-data', () => $fetch('/api/me'), {
  server: false,  // SSR 时不获取
})
text
SSR 时:跳过获取函数,data 为 undefined

客户端 Hydration 后:发起请求获取数据

data 变为实际值

INFO

server: falsedata 在 SSR 为 undefined 需要用 default 设置默认值,或在模板中处理空状态

ts
const { data } = await useAsyncData('me', () => $fetch('/api/me'), {
server: false,
default: () => ({ name: '', email: '' }),  // 避免模板中 data 为 undefined
})

选项

useAsyncData 的选项与 useFetch 几乎一样(因为 useFetch 就是 useAsyncData + $fetch 的封装):

选项默认值说明
servertrue是否在服务端执行
lazyfalse是否不阻塞导航
immediatetrue是否立即执行
defaultundefineddata 的默认值函数
transformundefined转换响应数据
pickundefined只提取指定字段
watchundefined监听源变化自动刷新
dedupe'cancel'去重策略:canceldefer
deepfalse是否深度 ref(返回数据的嵌套对象也是响应式的)
getCachedDataundefined自定义缓存读取逻辑

唯一区别

useAsyncData 没有 methodquerybodyheadersbaseURL 等 HTTP 选项(这些在获取函数中自己处理)。

deep 选项详解

ts
// 默认:deep: false
const { data } = await useAsyncData('user', () => $fetch('/api/user'))
// data.value 是浅响应式:顶层属性变化触发更新,嵌套属性变化不触发
data.value.name = 'new'  // ✅ 触发更新
data.value.settings.theme = 'dark'  // ❌ 不触发更新

// 开启深度响应式
const { data } = await useAsyncData('user', () => $fetch('/api/user'), {
  deep: true,
})
data.value.settings.theme = 'dark'  // ✅ 触发更新

INFO

性能提示deep: true 会递归遍历数据对象创建响应式代理 数据量大时可能影响性能。大多数情况下 deep: false 够用,如果需要修改嵌套数据,用 triggerRef(data) 或重新赋值

getCachedData 自定义缓存

ts
const { data } = await useAsyncData('posts', () => $fetch('/api/posts'), {
  getCachedData(key, nuxtApp) {
    const cached = nuxtApp.payload.data[key]
    if (!cached) return undefined

    // 5 分钟内使用缓存
    const expirationDate = new Date(cached.fetchedAt)
    expirationDate.setMinutes(expirationDate.getMinutes() + 5)
    if (new Date() < expirationDate) {
      return cached  // 返回缓存数据,不发请求
    }

    return undefined  // 缓存过期,重新请求
  },
})

getCachedData 的工作原理

如果这个函数返回了数据,useAsyncData 就不会执行获取函数,直接使用返回的缓存数据。这是实现自定义缓存策略的核心。

使用场景

1. 聚合多个 API

ts
const { data } = await useAsyncData('posts-page', async () => {
  const [posts, categories] = await Promise.all([
    $fetch('/api/posts'),
    $fetch('/api/categories'),
  ])
  return { posts, categories }
})

为什么不用两个 useFetch

也可以,但 useAsyncData 的好处是:

  • 只有一个 key,管理更简单
  • 可以在获取函数中处理两个请求之间的依赖关系
  • 一次 refresh() 刷新所有数据

2. 依赖请求

ts
const { data: categories } = await useFetch('/api/categories')
const selectedCategory = ref(categories.value?.[0]?.id)

// 第二个请求依赖第一个的结果
const { data: products } = await useAsyncData(
  'products-by-category',
  () => $fetch('/api/products', {
    query: { categoryId: selectedCategory.value },
  }),
  { watch: [selectedCategory] }
)

3. 使用第三方 SDK

ts
import { supabase } from '~/server/utils/supabase'

const { data } = await useAsyncData('profile', () =>
  supabase.from('profiles').select('*').eq('id', userId).single()
)

为什么第三方 SDK 要用 useAsyncData

useFetch 只能用于 HTTP 请求。Supabase、Firebase 等 SDK 有自己的获取方式,用 useAsyncData 包裹后也能享受 SSR 数据传递、去重、缓存等好处。

4. 使用 Nuxt Content

ts
const { data } = await useAsyncData('home-content', () =>
  queryContent('/').findOne()
)

5. 仪表盘数据聚合

ts
const { data } = await useAsyncData('dashboard', async () => {
  const [stats, recentOrders, notifications] = await Promise.all([
    $fetch('/api/stats'),
    $fetch('/api/orders/recent'),
    $fetch('/api/notifications'),
  ])
  return { stats, recentOrders, notifications }
})

createUseAsyncData(v4.4+)

类似 createUseFetch,创建自定义的 useAsyncData 实例,预设默认选项:

ts
// app/composables/useApiAsyncData.ts
export const useApiAsyncData = createUseAsyncData((options) => {
  return {
    ...options,
    server: false,  // 默认不在服务端获取
    deep: false,
  }
})

// 使用:和 useAsyncData 一样,但默认 server: false
const { data } = await useApiAsyncData('me', () => $fetch('/api/me'))

useNuxtData

通过 key 访问其他地方获取的缓存数据,不触发新请求

ts
// 在布局中获取全局配置
const { data: config } = await useAsyncData('app-config', () => $fetch('/api/config'))

// 在子组件中直接使用缓存(不再请求)
const { data: config } = useNuxtData('app-config')

useNuxtData 的典型场景

  • 根组件获取全局配置,子组件通过 useNuxtData 使用
  • 中间件中获取数据,页面中直接使用
  • 避免多个组件重复请求同一个 API

INFO

如果 key 对应的数据不存在useNuxtData 返回 undefined。确保数据已经被获取过再使用

useAsyncData vs useFetch 完整对比

特性useFetchuseAsyncData
获取方式固定用 $fetch自定义获取函数
key自动生成(基于 URL)必须手动指定
HTTP 选项method/query/body无(在获取函数中处理)
类型推断✅ 基于自动生成⚠️ 需要手动指定或依赖推断
适用场景标准 API 请求自定义获取逻辑
底层实现封装了 useAsyncData底层 API

选择策略

  • 需要发 HTTP 请求?→ 先试 useFetch
  • useFetch 不够灵活?→ 用 useAsyncData + $fetch
  • 不是 HTTP 请求?→ 用 useAsyncData

常见问题

key 冲突导致数据错乱

不同页面使用相同 key 会导致数据互相覆盖:

ts
// ❌ 页面 A 和页面 B 都用 'items' 作为 key
// 页面 A 获取用户列表,页面 B 获取商品列表
// 客户端导航时可能看到 A 的数据在 B 页面显示
const { data } = await useAsyncData('items', () => $fetch('/api/users'))

// ✅ 使用有语义的 key
const { data } = await useAsyncData('users-list', () => $fetch('/api/users'))

获取函数中有副作用

ts
// ❌ 错误:获取函数有副作用
const { data } = await useAsyncData('cart', async () => {
  await $fetch('/api/cart/clear')  // 副作用:清空购物车!
  return $fetch('/api/cart')
})

// ✅ 正确:获取函数只获取数据
const { data } = await useAsyncData('cart', () => $fetch('/api/cart'))

INFO

获取函数可能被执行多次(SSR 一次、客户端导航时又执行) 副作用操作(如写入、删除)会被重复执行。获取函数应该是纯读取操作

SSR 时数据未传递到客户端

  1. 确认 server: true(默认值)
  2. 确认获取函数在服务端能正常执行(不依赖浏览器 API)
  3. 确认返回值是可序列化的(纯对象/数组,不能是函数、类实例)

知识脉络

text
useFetch → 你在这里:useAsyncData

             ├─→ 下一步:$fetch(非响应式请求)

             ├─→ 深入了解:缓存与刷新(key 的缓存机制)

             └─→ 深入了解:SSR 数据传递(Payload 如何传递数据)

基于 Nuxt 4 官方文档整理编写