TanStack Virtualized动态高度行窗口缩放时滚动跳变问题求助
解决TanStack Virtualized动态网格滚动跳变问题
核心问题分析
窗口缩放时列数动态变化导致行高改变,但虚拟器依赖固定estimateSize预估行高,无法匹配真实行高,进而引发滚动跳变、不流畅。
解决方案步骤
1. 存储真实行高
创建响应式对象记录每行实际测量高度,避免重复预估:
const rowHeights = ref<Record<number, number>>({});
2. 实现行高测量函数
通过行索引定位DOM元素,获取真实高度并更新虚拟器尺寸:
const measureRowHeight = (rowIndex: number) => { if (!containerRef.value) return; const rowElement = containerRef.value.querySelector(`[data-row-index="${rowIndex}"]`); if (rowElement) { const height = rowElement.getBoundingClientRect().height; rowHeights.value[rowIndex] = height; rowVirtualizer.value.updateSize(rowIndex, height); } };
3. 动态更新预估尺寸
修改virtualizerOptions的estimateSize,优先使用已测量的真实高度,未测量时回退到预估值:
const virtualizerOptions = computed(() => ({ count: rowCount.value, getScrollElement: () => containerRef.value, estimateSize: (index) => rowHeights.value[index] ?? 480, overscan: 5, }));
4. 监听列数变化与窗口resize
在列数切换或窗口缩放后,重新测量可见行高度并通知虚拟器重新计算总尺寸:
// 监听列数变化 watch(columnCount, () => { rowHeights.value = {}; nextTick(() => { virtualRows.value.forEach(row => measureRowHeight(row.index)); rowVirtualizer.value.recomputeSizes(); }); }); // 监听窗口resize useEventListener('resize', () => { nextTick(() => { virtualRows.value.forEach(row => measureRowHeight(row.index)); rowVirtualizer.value.recomputeSizes(); }); });
5. 模板标记行索引
为虚拟行添加data-row-index属性,方便测量函数定位DOM:
<template v-for="row in virtualRows" :key="row.key" :data-row-index="row.index"> <template v-for="colIndex in columnCount" :key="colIndex"> <template v-if="(row.index * columnCount + colIndex - 1) < totalCardCount"> <CardComponent :key="cardIDs[row.index * columnCount + colIndex - 1]" v-if="cardMap.has(cardIDs[row.index * columnCount + colIndex - 1])" :item="cardMap.get(cardIDs[row.index * columnCount + colIndex - 1])!" /> </template> </template> </template>
6. 处理卡片异步加载(可选)
如果卡片内容异步加载,在卡片渲染完成后触发对应行的高度测量:
在CardComponent中添加自定义事件:
// CardComponent.vue defineEmits(['loaded']); onMounted(() => { emit('loaded'); });
父组件监听事件并触发测量:
<CardComponent :key="cardIDs[row.index * columnCount + colIndex - 1]" v-if="cardMap.has(cardIDs[row.index * columnCount + colIndex - 1])" :item="cardMap.get(cardIDs[row.index * columnCount + colIndex - 1])!" @loaded="measureRowHeight(row.index)" />
修改后的完整代码
<script setup lang="ts"> import { ref, computed, watch, nextTick } from 'vue'; import { useBreakpoints, useEventListener } from '@vueuse/core'; import { useVirtualizer } from '@tanstack/vue-virtual'; import { totalCardCount, cardIDs, cardMap } from '../data/generate'; import CardComponent from './CardComponent.vue'; defineProps<{ isVaultLoaded: boolean; }>(); const containerRef = ref<HTMLElement | null>(null); const rowHeights = ref<Record<number, number>>({}); const breakpoints = useBreakpoints({ sm: 740, md: 1020, lg: 1340, xl: 1640 }); const columnCount = computed(() => { if (breakpoints.smaller('sm').value) return 1; if (breakpoints.between('sm', 'md').value) return 2; if (breakpoints.between('md', 'lg').value) return 3; if (breakpoints.between('lg', 'xl').value) return 4; return 5; }); const rowCount = computed(() => Math.ceil(totalCardCount / columnCount.value)); const measureRowHeight = (rowIndex: number) => { if (!containerRef.value) return; const rowElement = containerRef.value.querySelector(`[data-row-index="${rowIndex}"]`); if (rowElement) { const height = rowElement.getBoundingClientRect().height; rowHeights.value[rowIndex] = height; rowVirtualizer.value.updateSize(rowIndex, height); } }; const virtualizerOptions = computed(() => ({ count: rowCount.value, getScrollElement: () => containerRef.value, estimateSize: (index) => rowHeights.value[index] ?? 480, overscan: 5, })); const rowVirtualizer = useVirtualizer(virtualizerOptions); const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems()); // 监听列数变化 watch(columnCount, () => { rowHeights.value = {}; nextTick(() => { virtualRows.value.forEach(row => measureRowHeight(row.index)); rowVirtualizer.value.recomputeSizes(); }); }); // 监听窗口resize useEventListener('resize', () => { nextTick(() => { virtualRows.value.forEach(row => measureRowHeight(row.index)); rowVirtualizer.value.recomputeSizes(); }); }); </script> <template> <div v-if="isVaultLoaded" ref="containerRef" class="grid-container hide-scrollbar"> <div :style="{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative' }"> <div class="grid-window @container" :style="{ '--column-count': columnCount, transform: `translateY(${virtualRows[0]?.start ?? 0}px)`, }" > <template v-for="row in virtualRows" :key="row.key" :data-row-index="row.index"> <template v-for="colIndex in columnCount" :key="colIndex"> <template v-if="(row.index * columnCount + colIndex - 1) < totalCardCount"> <CardComponent :key="cardIDs[row.index * columnCount + colIndex - 1]" v-if="cardMap.has(cardIDs[row.index * columnCount + colIndex - 1])" :item="cardMap.get(cardIDs[row.index * columnCount + colIndex - 1])!" @loaded="measureRowHeight(row.index)" /> </template> </template> </template> </div> </div> </div> <div v-else class="placeholder-text"> <p>Please select and load a vault to view cards.</p> </div> </template> <style scoped> .grid-container { width: 100%; height: 100%; overflow-y: auto; contain: strict; } .grid-window { position: absolute; top: 0; left: 0; width: 100%; display: grid; grid-template-columns: repeat(var(--column-count), 1fr); gap: 1rem; padding: 1rem; grid-template-rows: repeat(auto-fill, auto); } .placeholder-text { display: flex; align-items: center; justify-content: center; height: 100%; color: #888; font-style: italic; } .hide-scrollbar { scrollbar-width: none; -ms-overflow-style: none; } .hide-scrollbar::-webkit-scrollbar { display: none; } </style>
内容的提问来源于stack exchange,提问作者Andrew
相关产品推荐
相关产品推荐

