ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Vue Query 无限查询(Infinite Queries)完整指南:useInfiniteQuery 用法、响应式返回值与底层原理

Vue Query 无限查询(Infinite Queries)完整指南:useInfiniteQuery 用法、响应式返回值与底层原理 Vue Query 无限查询Infinite Queries完整指南useInfiniteQuery 用法、响应式返回值与底层原理【免费下载链接】query Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query无限查询Infinite Queries是 TanStack Vue Query 中用于实现加载更多、无限滚动等增量列表场景的核心能力它基于useQuery派生出useInfiniteQuery在保留全部查询特性的同时将数据组织为分页组pages并暴露向前/向后翻页的控制函数。本文以 Vue 框架的 infinite-queries 指南 为主体结合 React 侧完整版 infinite-queries.md 与仓库源码系统讲解useInfiniteQuery的完整配置项、实战写法、常见问题与底层实现读完即可在 Vue 3 项目中落地一个健壮的加载更多列表。使用 useInfiniteQuery 后有哪些变化在 Vue 组件中使用useInfiniteQuery时它被封装在 useInfiniteQuery.ts 中本质上是通过useBaseQuery配合InfiniteQueryObserver实现的与普通的useQuery相比你会发现以下几点明显不同data不再是单纯的响应数据而是一个包含无限查询数据的对象data.pages数组存放所有已拉取的分页组每个元素是一次 queryFn 的返回值data.pageParams数组存放拉取每一页时所使用的页码参数page param。新增fetchNextPage与fetchPreviousPage两个函数其中fetchNextPage为必用核心函数。新增initialPageParam选项必填用于指定第一个页面的页码参数。新增getNextPageParam与getPreviousPageParam选项既用于判断是否还有更多数据可加载也用于计算加载下一页所需的信息该信息会作为额外参数传给 queryFn。新增hasNextPage布尔值当getNextPageParam返回除null或undefined之外的值时为true。新增hasPreviousPage布尔值当getPreviousPageParam返回除null或undefined之外的值时为true。新增isFetchingNextPage与isFetchingPreviousPage布尔值用于区分后台刷新与加载更多两种拉取状态。注意如果你使用initialData或placeholderData为无限查询提供初始数据它们必须符合与真实数据相同的结构——即一个包含data.pages与data.pageParams两个属性的对象否则无限查询的状态计算会出错。完整示例基于 cursor 的 Load More 列表假设我们的 API 每次基于cursor游标返回 3 条projects数据同时返回一个可用于拉取下一组的游标fetch(/api/projects?cursor0) // { data: [...], nextCursor: 3} fetch(/api/projects?cursor3) // { data: [...], nextCursor: 6} fetch(/api/projects?cursor6) // { data: [...], nextCursor: 9} fetch(/api/projects?cursor9) // { data: [...] }基于这个信息构建 Load More UI 只需三步等待useInfiniteQuery默认发起首次请求拿到第一组数据在getNextPageParam中返回下一页的查询信息在按钮点击时调用fetchNextPage。下面是原文档中的完整 Vue SFC 示例见 infinite-queries.mdscript setup import { useInfiniteQuery } from tanstack/vue-query const fetchProjects async ({ pageParam }) { const res await fetch(/api/projects?cursor pageParam) return res.json() } const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, isPending, isError, } useInfiniteQuery({ queryKey: [projects], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) lastPage.nextCursor, }) /script template span v-ifisPendingLoading.../span span v-else-ifisErrorError: {{ error.message }}/span div v-else-ifdata span v-ifisFetching !isFetchingNextPageFetching.../span ul v-for(group, index) in data.pages :keyindex li v-forproject in group.projects :keyproject.id {{ project.name }} /li /ul button click() fetchNextPage() :disabled!hasNextPage || isFetchingNextPage span v-ifisFetchingNextPageLoading more.../span span v-else-ifhasNextPageLoad More/span span v-elseNothing more to load/span /button /div /template模板中的渲染逻辑清晰地区分了五种状态isPending首次请求尚未完成显示 Loading...isError请求失败展示error.messageisFetching !isFetchingNextPage正在进行后台刷新而非加载更多时显示 Fetching...isFetchingNextPage正在加载下一页时按钮显示 Loading more... 且处于禁用状态hasNextPage为假时按钮显示 Nothing more to load表示已没有更多数据。每一组data.pages中的元素对应一次 queryFn 的返回结果本例中为{ data: [...], nextCursor }因此内层遍历使用的是group.projects。Vue Query 的响应式返回值ref 与函数的区别在 Vue 中useInfiniteQuery的返回值类型与 React 版本有明显差异理解这一点对正确书写模板与逻辑至关重要。查看 useBaseQuery.ts 中定义的UseBaseQueryReturnTypedata、error、hasNextPage、isFetching、isPending等状态字段全部是Ref在模板中会被自动解包所以v-ifisPending可以直接书写在script中则需要data.value或保持解构后的 ref 使用fetchNextPage、fetchPreviousPage、refetch则是普通函数不是 ref可以直接调用。其实现原理位于 useBaseQuery.tsVue Query 内部创建InfiniteQueryObserver实例用reactive()包装其当前结果作为state通过observer.subscribe在结果变化时调用updateState更新响应式状态最后用toRefs(readonlyState)将状态字段转为 ref而对typeof state[key] function的字段即函数则直接原样挂载到返回对象上。此外Vue Query 还提供了一些平台相关的细节若在setup()或 effect scope 之外调用useInfiniteQuery开发环境下会打印内存泄漏警告useBaseQuery.ts可通过shallow: true选项让内部使用shallowReactive/shallowReadonly减少深层响应式代理开销返回对象上还附带suspense()方法可在Suspense场景下手动等待查询结果。分页参数如何计算getNextPageParam 的完整签名与 hasNextPage 判定getNextPageParam并不是只接收lastPage一个参数。查看核心实现 infiniteQueryBehavior.tsfunction getNextPageParam(options, { pages, pageParams }) { const lastIndex pages.length - 1 return pages.length 0 ? options.getNextPageParam( pages[lastIndex], // lastPage pages, // 全部页面 pageParams[lastIndex], // 最后一页的 pageParam pageParams, // 全部 pageParams ) : undefined }也就是说getNextPageParam的实际签名是(lastPage, allPages, lastPageParam, allPageParams)getPreviousPageParam对应为(firstPage, allPages, firstPageParam, allPageParams)。多数场景只需用到第一个参数从响应中取nextCursor但当你需要基于当前页码自增或根据已有页面数量做判断时后面的参数就派上用场了。hasNextPage与hasPreviousPage的判定逻辑也非常直接infiniteQueryBehavior.ts调用对应的 pageParam 函数只要返回值! null既不是null也不是undefined即为true。因此你的getNextPageParam应当在没有更多数据时明确返回undefined而不是返回0之类的假值——否则hasNextPage仍会判定为true。避免并发拉取冲突fetchNextPage 与 isFetching 的配合必须理解一个关键事实同一个 InfiniteQuery 同时只能有一个进行中的 fetch。所有页面共享同一条缓存记录cache entry如果在已有 fetch 进行时再次触发fetchNextPage可能会导致正在后台发生的数据刷新被覆盖。这在渲染列表的同时触发fetchNextPage的场景下尤其危险。如果你确实需要允许同时拉取可以在fetchNextPage中传入{ cancelRefetch: false }该选项默认值为true。但更稳妥的推荐做法是在触发加载更多之前先确认查询不处于isFetching状态尤其当该调用不是由用户直接控制时List onEndReached{() hasNextPage !isFetching fetchNextPage()} /这个模式同样适用于 Vue 项目中的无限滚动指令或 IntersectionObserver 回调。无限查询重新获取refetch时会发生什么当无限查询变stale需要重新获取时每个分组会被按顺序sequentially从头开始依次拉取而不是并行发起。这样做的原因在于如果底层数据发生了变化继续使用旧的游标可能造成重复数据或遗漏记录。这个行为可以在 infiniteQueryBehavior.ts 中看到在没有direction即非加载更多场景时代码从initialPageParam或已有第一页参数开始循环调用fetchPage直到拉完remainingPages默认等于已有页面数。此外如果无限查询的结果被从 queryCache 中移除例如gcTime过期被回收或手动queryClient.removeQueries分页状态会重置到初始状态仅重新请求第一组数据。双向无限列表getPreviousPageParam / fetchPreviousPage需要实现类似聊天记录向上翻页的双向列表时可以使用getPreviousPageParam、fetchPreviousPage、hasPreviousPage与isFetchingPreviousPageuseInfiniteQuery({ queryKey: [projects], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) firstPage.prevCursor, })从源码 infiniteQueryBehavior.ts 可以看到fetchNextPage与fetchPreviousPage会通过meta: { fetchMore: { direction } }标记拉取方向forward/backward。InfiniteQueryObserver.createResultinfiniteQueryObserver.ts根据这个方向计算isFetchingNextPage、isFetchNextPageError、isFetchingPreviousPage、isFetchPreviousPageError等状态向前翻页时新页面会被追加到pages头部addToStart向后翻页时追加到尾部addToEnd。倒序展示页面select 选项如果你希望页面以倒序展示比如最新的内容在顶部可以使用select选项对数据进行变换useInfiniteQuery({ queryKey: [projects], queryFn: fetchProjects, select: (data) ({ pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse(), }), })注意select的返回值必须同时包含pages与pageParams且数组反转时应保持一致否则后续的翻页计算会基于错误的pageParams顺序。手动更新无限查询数据setQueryData 的三种场景当 API 发生变更、需要本地乐观更新无限查询数据时使用queryClient.setQueryData并保持{ pages, pageParams }结构不变即可。手动移除第一页例如聊天中的加载更早消息合并queryClient.setQueryData([projects], (data) ({ pages: data.pages.slice(1), pageParams: data.pageParams.slice(1), }))手动从某一页中移除单条记录例如删除一条数据后本地过滤const newPagesArray oldPagesArray?.pages.map((page) page.filter((val) val.id ! updatedId), ) ?? [] queryClient.setQueryData([projects], (data) ({ pages: newPagesArray, pageParams: data.pageParams, }))只保留第一页例如重置为初始状态queryClient.setQueryData([projects], (data) ({ pages: data.pages.slice(0, 1), pageParams: data.pageParams.slice(0, 1), }))无论哪种场景请务必始终维护pages与pageParams一一对应的数据结构否则翻页与重取逻辑会失效。限制页面数量maxPagesLimited Infinite Query在某些场景下你可能希望限制查询数据中保存的页面数量以兼顾性能与体验用户可能加载大量页面内存占用需要重新获取包含几十页的无限查询时所有页面会被顺序重新拉取网络开销。解决方案是使用受限无限查询配合maxPages选项在getNextPageParam与getPreviousPageParam同时存在的前提下允许在需要时向前后两个方向拉取页面。下面的示例中查询数据只保留 3 页如果需要重取也只会顺序重取这 3 页useInfiniteQuery({ queryKey: [projects], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) firstPage.prevCursor, maxPages: 3, })底层实现中addToEnd/addToStart两个工具函数见 infiniteQueryBehavior.ts在追加新页时会把超过maxPages的旧页裁掉从而让pages数组始终保持在限定长度内。API 没有 cursor 怎么办用 pageParam 作为游标如果后端 API 不返回游标可以直接把pageParam当作游标使用。因为getNextPageParam与getPreviousPageParam还能拿到当前页的pageParam你完全可以用它计算下一页/上一页的参数useInfiniteQuery({ queryKey: [projects], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, allPages, lastPageParam) { if (lastPage.length 0) { return undefined } return lastPageParam 1 }, getPreviousPageParam: (firstPage, allPages, firstPageParam) { if (firstPageParam 1) { return undefined } return firstPageParam - 1 }, })这里lastPage.length 0表示 API 返回了空数组即没有更多数据此时返回undefined让hasNextPage变为false而firstPageParam 1表示已经回到第一页返回undefined关闭向上翻页。进阶资源预取、类型化选项与测试用例如果你需要在实际项目中进一步使用无限查询仓库还提供了以下配套能力usePrefetchInfiniteQuery.ts在组件渲染前预取无限查询配合 SSR 或路由守卫使用infiniteQueryOptions.ts提供类型安全的infiniteQueryOptions()构造器便于在组件外集中定义并复用无限查询选项useInfiniteQuery.test.ts 与 infiniteQueryBehavior.test.tsx覆盖了分页参数计算、maxPages裁剪、双向翻页、重取顺序等行为的测试用例是理解各种边界行为的绝佳参考核心类型定义集中在 infiniteQueryObserver.ts观察者实现与 types.tsInfiniteData、FetchNextPageOptions等类型。综上useInfiniteQuery在 Vue Query 中提供了一套完整、声明式的无限列表解决方案只要定义好initialPageParam与getNextPageParam数据的分页获取、去重、重取与状态标记全部由框架接管配合maxPages、select、setQueryData等能力可以覆盖从简单的 Load More 到双向聊天记录、受限分页等绝大多数生产场景。【免费下载链接】query Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进