人人都会AI编程

数据缓存、自动重取、乐观更新、分页 / 无限滚动

更新时间:2026-07-09

在现代前端开发中,服务端数据的请求状态管理往往比组件内部状态更复杂。传统的 Axios 封装虽然能统一处理请求/响应,但缺乏对服务端数据生命周期的统一管理——开发者需要自己维护 loading、error、data,自己决定何时刷新数据,多人协作时还容易出现重复请求。TanStack Query(Vue Query)将这类“服务端状态”抽象为一种独立的、带有缓存和自动更新机制的资源,让开发者从这些繁琐的模式中解脱出来。

数据缓存:让服务器数据“停留”在客户端

每次从服务器获取数据后,TanStack Query 会将结果以 key(查询标识) 为索引存入内存缓存。当同一个 key 再次触发查询时,它会先立即返回缓存中的数据(如果存在),同时根据配置决定是否在后台重新请求并更新缓存。这带来了两个直接好处:

  • 秒开的页面切换:用户从列表页进入详情页再返回时,列表数据不会“消失”然后重新加载,而是直接展示上一次请求的结果,视觉上几乎没有等待。
  • 共享数据源:多个组件使用同一个查询 key 时,只会发送一次网络请求,数据被所有组件共享,无需通过组件树传参或存入全局 Store。
// 一个简单的缓存查询示例
const { data, isLoading, error } = useQuery({
  queryKey: ['todos'],
  queryFn: () => axios.get('/api/todos').then(res => res.data),
  staleTime: 1000 * 60 * 5, // 5分钟内数据视为“新鲜”,不自动重取
})

配置项 staleTime 决定了数据的新鲜度:在新鲜期内,组件重新挂载或重新聚焦页面时不会触发请求;超过此时间的数据会被标记为“过时”,并在下一次被使用时触发后台重取。

自动重取:让界面保持“最新”

很多场景下,用户期望看到的数据不应该是上一次打开页面时的快照,而是应当和服务器保持同步。TanStack Query 提供了多种自动重取策略,无需手动监听事件或轮询:

  • 窗口聚焦重取(默认开启):用户切走浏览器标签页再切回来时,自动后台刷新数据。
  • 网络重连重取:断网重连后自动重新请求之前未成功的数据。
  • 定时轮询:通过 refetchInterval 设置毫秒级定时更新,适合股票、天气等实时性要求高的场景。
  • 根据 staleTime 自动后台刷新:数据一旦过时,下一次被读取时会自动触发请求。

这些机制让数据保持“准实时”的同时,开发者无需在组件中编写任何定时器或全局事件监听——查询一旦定义,生命周期就由框架接管。

乐观更新:让操作“瞬间完成”

在传统的 mutation(创建、编辑、删除)流程中,操作通常是:前端发送请求 → 等待服务器响应 → 响应成功后再更新本地数据。这导致的体验是用户点击按钮后必须等待网络往返,界面才发生变化,在弱网下尤为明显。

乐观更新的做法是:先假设请求一定会成功,立即更新 UI 并回滚到修改前的状态,同时发送实际请求。如果请求成功,保持更新后的状态;如果失败,将 UI 恢复到修改之前。用户感知到的操作延迟几乎为零。

const queryClient = useQueryClient()
const mutation = useMutation({
  mutationFn: (newTodo) => axios.post('/api/todos', newTodo),
  // 乐观更新配置
  onMutate: async (newTodo) => {
    // 取消进行中的查询,避免旧数据覆盖乐观更新
    await queryClient.cancelQueries({ queryKey: ['todos'] })
    // 保存当前列表数据(用于回滚)
    const previousTodos = queryClient.getQueryData(['todos'])
    // 乐观地更新列表
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
    // 返回上下文供 onError 回滚
    return { previousTodos }
  },
  onError: (err, newTodo, context) => {
    // 请求失败,回滚到之前的数据
    queryClient.setQueryData(['todos'], context.previousTodos)
  },
  onSettled: () => {
    // 无论成功或失败,确保列表数据最终与服务端一致
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})

这种交互在类似点赞、收藏、即时聊天、拖拽排序等场景中能够大幅提升用户体验,但它也需要开发者仔细处理回滚逻辑,避免页面状态与服务器长期不一致。

分页与无限滚动:低开销的长列表数据

大数据列表不可能一次性全部加载,分页和无限滚动是两种最常用的浏览方式。TanStack Query 对这两种模式都提供了开箱即用的支持:

  • 分页:通过将页码作为查询 key 的一部分(例如 ['posts', page]),不同页数据会被独立缓存。切换页码时,若该页数据已缓存则立即显示,同时后台仍会按需重取。
const page = ref(1)
const { data } = useQuery({
  queryKey: ['posts', page],
  queryFn: () => axios.get(`/api/posts?page=${page.value}`),
  keepPreviousData: true,   // 翻页时保留旧数据避免闪烁
})
  • 无限滚动:使用 useInfiniteQuery,框架会自动管理“下一页”的游标(如 offset 或 cursor),并合并多页数据为一条扁平列表。当用户滚动到底部触发加载更多时,只需调用 fetchNextPage(),之前的页面不需要重新请求。
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam = 0 }) =>
    axios.get(`/api/projects?cursor=${pageParam}`).then(res => res.data),
  getNextPageParam: (lastPage) => lastPage.nextCursor,  // 返回下一页的游标
})

这两种模式下,缓存和自动重取依然有效:滚动回顶部时之前加载的数据不会丢失,切走再回来也不会重复请求已经拥有的数据。

实用提示:这些功能的底层思想是把“请求来的数据”当作一种可以缓存、自动失效、按需订阅的资源来管理。真正要用好它们,关键是为每个查询设定合理的 staleTime 和缓存时长,并对乐观更新做好出错回滚。其余的事情,框架已经替你处理好了。