useFetch 与 useAsyncData 深度对比

useFetchuseAsyncData 是 Nuxt4 中获取数据的两大核心 Composable——看似功能相近,实则设计意图、底层机制、适用场景各有不同。本章从源码层面讲清两者的关系,深入 key 去重、transform/pick 数据裁剪、getCachedData 缓存策略,以及 lazy/server/immediate 等关键参数的行为差异。

1. 底层原理差异

1.1 两者的关系

// 简化版源码关系
function useFetch(url, options) {
  return useAsyncData(
    key,                          // 自动生成的唯一 key
    () => $fetch(url, options),   // 封装了 $fetch 调用
    options                       // 透传选项
  )
}

useFetch = useAsyncData + $fetchuseFetch 是一个便捷封装,内部调用 useAsyncData,并自动使用 $fetch 作为数据获取函数。

1.2 核心差异

特性useFetchuseAsyncData
数据获取方式内置 $fetch(HTTP 请求)自定义函数(任意异步操作)
URL 响应式✅ URL/参数变化自动重新获取需手动 watch
请求去重✅ SSR → CSR 自动去重✅ 相同 key 去重
SSR 优化✅ 服务端直接内部调用(不走 HTTP)
适用场景API 请求(绝大多数场景)非 HTTP 数据源(SDK、计算等)

1.3 useFetch 的 SSR 智能优化

// 在 SSR 模式下,useFetch 调用内部 API 时
// 不会发起真正的 HTTP 请求,而是直接调用服务端函数
const { data } = await useFetch('/api/videos')
 
// SSR 时的内部行为:
// ❌ 不会:HTTP GET http://localhost:3000/api/videos
// ✅ 实际:直接调用 server/api/videos.ts 的处理函数
// 这避免了网络开销,大幅提升 SSR 性能

1.4 何时用 useAsyncData

当数据源不是 HTTP API 时:

// 场景 1:从第三方 SDK 获取数据
const { data: user } = await useAsyncData('firebase-user', () => {
  return firebaseAuth.currentUser
})
 
// 场景 2:组合多个请求
const { data } = await useAsyncData('dashboard', async () => {
  const [videos, stats, notifications] = await Promise.all([
    $fetch('/api/videos/mine'),
    $fetch('/api/stats'),
    $fetch('/api/notifications'),
  ])
  return { videos, stats, notifications }
})
 
// 场景 3:从本地数据库/IndexedDB 读取
const { data: cache } = await useAsyncData('local-cache', () => {
  return localDB.getAll('drafts')
})
 
// 场景 4:复杂计算
const { data: report } = await useAsyncData('report', async () => {
  const raw = await $fetch('/api/analytics/raw')
  return computeExpensiveReport(raw)  // CPU 密集计算
})

2. key 的去重机制

2.1 key 的作用

每个 useFetch/useAsyncData 调用都有一个唯一的 key,用于:

  1. SSR → CSR 去重:服务端获取的数据通过 key 存入 Payload,客户端通过 key 读取,避免重复请求
  2. 同页面去重:同一个 key 的多次调用只发起一次请求
  3. 缓存标识getCachedData 通过 key 查找缓存

2.2 useFetch 的自动 key

useFetch 会根据 URL 和参数自动生成 key

// 自动生成的 key 基于:URL + method + query + body
useFetch('/api/videos')
// key ≈ '$f/api/videos'
 
useFetch('/api/videos', { query: { page: 1 } })
// key ≈ '$f/api/videos:page=1'
 
useFetch('/api/videos/123')
// key ≈ '$f/api/videos/123'

2.3 useAsyncData 必须手动指定 key

// ✅ 必须提供唯一 key
const { data } = await useAsyncData('dashboard-stats', () =>
  $fetch('/api/stats')
)
 
// ❌ 不提供 key 会使用文件名 + 行号作为 key(不稳定,不推荐)
const { data } = await useAsyncData(() => $fetch('/api/stats'))

2.4 自定义 key

// 手动指定 key(覆盖自动生成)
const route = useRoute()
const { data } = await useFetch('/api/videos', {
  key: `videos-page-${route.query.page}`,
  query: { page: route.query.page },
})

2.5 key 冲突问题

// ⚠️ 两个组件使用相同的 URL 但期望不同结果时
// 组件 A
const { data: trending } = await useFetch('/api/videos', {
  query: { sort: 'trending' },
})
 
// 组件 B
const { data: latest } = await useFetch('/api/videos', {
  query: { sort: 'latest' },
})
// ✅ 不会冲突,因为 query 不同,自动生成的 key 也不同
 
// ⚠️ 但如果 query 是响应式的,初始值可能相同
const sort = ref('trending')
const { data } = await useFetch('/api/videos', {
  key: `videos-${sort.value}`,  // ❌ 初始值固定,sort 变化时 key 不变
  query: { sort },
})
// ✅ 修复:让 useFetch 自动管理 key,或使用 watch: false + 手动 refresh

3. transform / pick / getCachedData 高级用法

3.1 transform — 数据转换

在数据返回后、存入 Payload 前进行转换:

// 场景:API 返回嵌套结构,只需要核心数据
const { data: videos } = await useFetch('/api/videos', {
  transform: (response) => {
    // response = { code: 200, data: [...], meta: { total: 100 } }
    return response.data.map(v => ({
      id: v.id,
      title: v.title,
      thumbnail: v.thumbnails?.medium?.url,
      duration: formatDuration(v.duration),
    }))
  },
})
// data.value = [{ id, title, thumbnail, duration }, ...]

关键transform 的结果会被序列化到 Payload,所以在这里精简数据可以减小 HTML 体积

3.2 pick — 字段选取

picktransform 的简化版——只选取指定字段:

const { data: video } = await useFetch(`/api/videos/${id}`, {
  pick: ['id', 'title', 'thumbnail', 'description'],
})
// API 可能返回 50 个字段,但 Payload 只包含这 4 个

pick 适合简单的字段过滤,transform 适合复杂的数据变换。

3.3 getCachedData — 自定义缓存策略

getCachedData 让你控制何时使用缓存、何时重新获取

const { data } = await useFetch('/api/videos/trending', {
  // 如果缓存存在且未过期,直接使用缓存
  getCachedData(key, nuxtApp) {
    const cached = nuxtApp.payload.data[key] || nuxtApp.static.data[key]
    if (!cached) return null  // 没有缓存,发起请求
 
    // 检查缓存是否过期(5 分钟)
    const cachedAt = nuxtApp.payload._cache?.[key]
    if (cachedAt && Date.now() - cachedAt > 5 * 60 * 1000) {
      return null  // 缓存过期,重新获取
    }
 
    return cached  // 使用缓存
  },
})

常见缓存模式

// 模式 1:永不过期(整个会话期间只请求一次)
getCachedData(key, nuxtApp) {
  return nuxtApp.payload.data[key] || nuxtApp.static.data[key]
}
 
// 模式 2:TTL 过期
getCachedData(key, nuxtApp) {
  const data = nuxtApp.payload.data[key]
  if (!data) return null
  const expiry = nuxtApp.payload._expiry?.[key]
  if (expiry && Date.now() > expiry) return null
  return data
}
 
// 模式 3:返回 stale 数据并在后台刷新(SWR 风格)
// 使用 dedupe: 'defer' 配合

4. lazy / server / immediate 参数详解

4.1 参数行为矩阵

参数默认值含义
lazyfalsetrue = 不阻塞导航,异步加载
servertruefalse = 跳过服务端获取,仅客户端
immediatetruefalse = 不自动执行,手动调用 execute()
watch自动false = 响应式参数变化时不自动重新获取

4.2 lazy — 非阻塞数据获取

<script setup lang="ts">
// 默认:阻塞导航(页面等数据加载完才显示)
const { data: video } = await useFetch(`/api/videos/${id}`)
 
// lazy:不阻塞导航(页面先显示,数据后到)
const { data: comments, status } = useFetch(`/api/videos/${id}/comments`, {
  lazy: true,
})
// 注意:lazy 模式下不需要 await
</script>
 
<template>
  <div>
    <VideoPlayer :video="video" />
 
    <!-- 评论区:lazy 加载,需要处理 loading 状态 -->
    <div v-if="status === 'pending'">
      <CommentSkeleton v-for="i in 5" :key="i" />
    </div>
    <CommentList v-else :comments="comments" />
  </div>
</template>

useLazyFetchuseFetch(&#123; lazy: true &#125;) 的快捷方式:

// 两者等价
const { data } = useLazyFetch('/api/comments')
const { data } = useFetch('/api/comments', { lazy: true })

4.3 server — 跳过 SSR

// 仅在客户端获取(不在 SSR 时执行)
const { data: analytics } = useFetch('/api/analytics', {
  server: false,
})
 
// 适用场景:
// - 数据不影响 SEO(如用户行为分析)
// - 依赖浏览器 API(如 geolocation)
// - 减少 SSR 负载

4.4 immediate — 手动触发

const searchQuery = ref('')
 
// 不自动执行,等用户输入后手动触发
const { data: results, execute: search } = useFetch('/api/search', {
  immediate: false,
  query: { q: searchQuery },
})
 
// 用户点击搜索按钮时手动触发
async function handleSearch() {
  if (!searchQuery.value.trim()) return
  await search()
}

4.5 watch — 响应式参数自动重新获取

const page = ref(1)
const sort = ref('newest')
 
const { data: videos } = await useFetch('/api/videos', {
  query: { page, sort },
  // 默认:page 或 sort 变化时自动重新获取
})
 
// 禁用自动 watch
const { data, refresh } = await useFetch('/api/videos', {
  query: { page, sort },
  watch: false,  // 不自动重新获取
})
// 需要手动调用 refresh()

4.6 组合使用场景

// 场景 1:核心数据阻塞 + 次要数据懒加载
const { data: video } = await useFetch(`/api/videos/${id}`)  // 阻塞
const { data: related } = useFetch(`/api/videos/${id}/related`, { lazy: true })  // 懒加载
const { data: comments } = useFetch(`/api/videos/${id}/comments`, { lazy: true })
 
// 场景 2:搜索页 — 手动触发 + 仅客户端
const { data, execute } = useFetch('/api/search', {
  immediate: false,
  server: false,
  query: { q: searchQuery },
})
 
// 场景 3:仪表盘 — 定时刷新
const { data: stats, refresh } = await useFetch('/api/stats')
// 每 30 秒刷新一次
const interval = setInterval(() => refresh(), 30000)
onUnmounted(() => clearInterval(interval))

5. Payload 共享策略

5.1 什么是 Payload 共享

在 SSR 模式下,useFetch/useAsyncData 获取的数据会通过 Payload 从服务端传递到客户端:

服务端:
  useFetch('/api/videos') → 获取数据 → 存入 Payload

  HTML 响应:
  <script>window.__NUXT__ = { data: { "$f/api/videos": [...] } }</script>

客户端:
  useFetch('/api/videos') → 从 Payload 读取 → 不再发起请求

5.2 多组件共享同一数据

// 组件 A 和组件 B 都需要用户信息
// 方式 1:使用 useState(推荐用于全局状态)
const user = useState('user', () => null)
 
// 方式 2:使用相同的 key(useFetch 自动去重)
// 组件 A
const { data: user } = await useFetch('/api/auth/me')
// 组件 B(相同 URL → 相同 key → 复用数据,不会重复请求)
const { data: user } = await useFetch('/api/auth/me')

5.3 跨页面数据预取

// 在列表页预取详情页数据
const { data: videos } = await useFetch('/api/videos')
 
// 鼠标悬停时预取
function prefetchVideo(id: string) {
  // 预取数据,存入缓存
  useNuxtApp().runWithContext(() => {
    useFetch(`/api/videos/${id}`, {
      key: `video-${id}`,
      immediate: true,
    })
  })
}

5.4 返回值完整解构

const {
  data,       // Ref<T | null>   — 响应数据
  error,      // Ref<Error | null> — 错误信息
  status,     // Ref<'idle' | 'pending' | 'success' | 'error'> — 请求状态
  refresh,    // () => Promise<void> — 重新获取(使用缓存 key)
  execute,    // () => Promise<void> — 执行获取(同 refresh)
  clear,      // () => void — 清空数据和错误
} = await useFetch('/api/videos')
 
// status 的用法
if (status.value === 'pending') { /* 加载中 */ }
if (status.value === 'error') { /* 出错了 */ }
if (status.value === 'success') { /* 成功 */ }

本章小结

  • 关系useFetch = useAsyncData + $fetch,前者是便捷封装
  • 选择:HTTP API 用 useFetch;非 HTTP 数据源或多请求组合用 useAsyncData
  • key 机制useFetch 自动生成 key,useAsyncData 需手动指定;key 用于去重和缓存
  • transform/pick:在 Payload 序列化前精简数据,减小 HTML 体积
  • getCachedData:自定义缓存策略(TTL、SWR、永不过期)
  • lazy:不阻塞导航,适合次要数据
  • serverfalse 跳过 SSR,适合非 SEO 数据
  • immediatefalse 不自动执行,适合搜索等手动触发场景
  • Payload 共享:SSR 数据通过 key 传递到客户端,避免重复请求

下一章讲 $fetch 的底层封装与 BFF 接口层设计。