Pinia 不处理虚拟滚动的可视区域计算,只管理需持久化或跨组件共享的状态。运行时状态(如 scrollTop)应在组件内用 ref 管理;用户操作意图(如选中、展开)按 item ID 细粒度存入 Pinia;滚动位置可跨路由保存,但不可存储 visibleStartIndex 等渲染中间态。

Pinia 本身不直接处理虚拟滚动中“当前可视区域”的状态,因为它不感知 DOM、滚动位置或渲染逻辑。它只负责状态的定义、存储与响应式同步。可视区域的计算、item 的动态切片、滚动监听这些事,属于组件层面的逻辑控制,Pinia 只在需要持久化或共享这部分状态时才介入。
关键要分清两层职责:
虚拟滚动的运行时状态(如
scrollTop、visibleStartIndex、bufferSize)
这些是瞬时 UI 衍生数据,通常用ref或reactive在组件内管理即可,无需上抛到 Pinia —— 毕竟每次滚动都触发 store 更新,既冗余又影响性能。需要跨组件保持或恢复的状态(如用户对某几条列表项的临时勾选、折叠/展开状态、编辑中的草稿行)
这类“可视区域相关但语义明确”的状态,才适合交给 Pinia 管理,而且推荐按 item ID 做细粒度缓存。
✅ 场景一:记住用户对滚动中某几项的操作(如多选、展开)
-
在 Pinia 中为该列表页定义专属 store,用对象映射存储每项的 UI 状态:
// stores/listUI.ts export const useListUIStore = defineStore('listUI', () => { const itemStates = ref<Record<string, { expanded: boolean; selected: boolean }>>({}) const setItemState = (id: string, partial: Partial<typeof itemStates.value[string]>) => { itemStates.value[id] = { ...itemStates.value[id], ...partial } } const getItemState = (id: string) => itemStates.value[id] ?? { expanded: false, selected: false } return { itemStates, setItemState, getItemState } }) -
虚拟滚动组件中,在渲染每个 item 时读取并绑定:
<VirtualScroll :items="allItems" :item-size="64"> <template #default="{ item }"> <ListItem :item="item" :expanded="listUI.getItemState(item.id).expanded" :selected="listUI.getItemState(item.id).selected" @toggle-expanded="() => listUI.setItemState(item.id, { expanded: !listUI.getItemState(item.id).expanded })" @toggle-selected="() => listUI.setItemState(item.id, { selected: !listUI.getItemState(item.id).selected })" /> </template> </VirtualScroll>
⚠️ 注意:不要把整个
visibleItems数组存进 store,只存“用户意图”,比如{ '1001': { selected: true } }—— 轻量、可序列化、易清理。
✅ 场景二:滚动位置需要跨路由保留(比如返回列表页时回到原位置)
-
这类状态属于页面级“导航上下文”,适合存在页面专属 Pinia store 中:
// stores/orderList.ts export const useOrderListStore = defineStore('orderList', () => { const scrollTop = ref(0) const lastSearchKeyword = ref('') // 页面激活时恢复 function restoreScroll() { nextTick(() => { containerRef.value?.scrollTo({ top: scrollTop.value }) }) } return { scrollTop, lastSearchKeyword, restoreScroll } }) 在虚拟滚动容器的
onActivated中调用restoreScroll(),在onDeactivated中保存containerRef.value?.scrollTop。
❌ 不推荐的做法
- 把
visibleStartIndex/visibleEndIndex存进 Pinia 并试图用它驱动渲染 —— 这破坏了虚拟滚动的响应式闭环,容易和v-for切片逻辑冲突。 - 用
watch监听scrollTop并频繁 commit 到 store —— 触发太密,无实际业务意义,纯属性能负担。 - 所有列表页共用一个全局 store 存所有状态 —— 缺乏隔离,切换页面时状态互相污染。
Pinia 在虚拟滚动里的角色很清晰:它不替你算滚动,也不帮你复用 DOM,但它能稳稳托住那些“用户真正在意、且离开后还想回来”的状态。用得好,滚动体验丝滑;用错了,反而让简单逻辑变复杂。


















