useFetch 与 useAsyncData 深度对比
useFetch和useAsyncData是 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 + $fetch。useFetch 是一个便捷封装,内部调用 useAsyncData,并自动使用 $fetch 作为数据获取函数。
1.2 核心差异
| 特性 | useFetch | useAsyncData |
|---|---|---|
| 数据获取方式 | 内置 $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,用于:
- SSR → CSR 去重:服务端获取的数据通过 key 存入 Payload,客户端通过 key 读取,避免重复请求
- 同页面去重:同一个 key 的多次调用只发起一次请求
- 缓存标识:
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 + 手动 refresh3. 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 — 字段选取
pick 是 transform 的简化版——只选取指定字段:
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 参数行为矩阵
| 参数 | 默认值 | 含义 |
|---|---|---|
lazy | false | true = 不阻塞导航,异步加载 |
server | true | false = 跳过服务端获取,仅客户端 |
immediate | true | false = 不自动执行,手动调用 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>useLazyFetch 是 useFetch({ lazy: true }) 的快捷方式:
// 两者等价
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:不阻塞导航,适合次要数据
- server:
false跳过 SSR,适合非 SEO 数据 - immediate:
false不自动执行,适合搜索等手动触发场景 - Payload 共享:SSR 数据通过 key 传递到客户端,避免重复请求
下一章讲 $fetch 的底层封装与 BFF 接口层设计。