VirtualScroller 虚拟滚动 
比虚拟列表有更好的渲染性能。
组件注册 
js
import { FVirtualScroller } from 'fes-design';
app.use(FVirtualScroller);代码演示 
不规则纵向滚动 
play
<template> <FVirtualScroller ref="virtualList" class="virtual-scroll-list-vertical" :dataSources="dataItems" :itemSize="80" :height="height" > <template #default="{ source, index }"> <div :data-index="index" class="item-inner"> <div class="head"> <span># {{ source.index }}</span> <span>{{ source.name }}</span> </div> <div class="desc">{{ source.desc }}</div> </div> </template> </FVirtualScroller> <FButton style="margin-top: 10px;" @click="addMessage">添加消息{{ dataItems.length }}</FButton> </template> <script> import { ref } from 'vue'; export default { name: 'Vertical', setup() { const virtualList = ref(null); const height = ref(400); const sentence3 = [ 'BFC(Block formatting context)直译为"块级格式化上下文"。它是一个独立的渲染区域,只有Block-level box参与, 它规定了内部的Block-level Box如何布局,并且与这个区域外部毫不相干。', 'IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)', 'margin 重合,margin 塌陷', 'css3', 'html5IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)', 'es6', ]; const genUniqueId = (prefix) => { return `${prefix}$${Math.random().toString(16).substr(9)}`; }; const getSentences = () => { const index = Math.floor(Math.random() * (sentence3.length - 1)); return sentence3[index]; }; const TOTAL_COUNT = 1000; const dataItems = ref([]); let count = TOTAL_COUNT; while (count--) { const index = TOTAL_COUNT - count; dataItems.value.push({ index, name: `${Math.random()}`, id: genUniqueId(index), desc: getSentences(), }); } const addMessage = () => { const index = dataItems.value.length + 1; dataItems.value = [...dataItems.value, { index, name: `${Math.random()}`, id: genUniqueId(index), desc: getSentences(), }]; virtualList.value.scrollToBottom(); }; return { virtualList, dataItems, height, addMessage, }; }, }; </script> <style scoped> .virtual-scroll-list-vertical .item-inner .head { font-weight: 500; } .virtual-scroll-list-vertical .item-inner .head span:first-child { margin-right: 1em; } .virtual-scroll-list-vertical .item-inner .desc { margin-top: 0.5em; margin-bottom: 1em; text-align: justify; } </style>
不规则横向滚动 
play
<template> <FVirtualScroller class="virtual-scroll-list-horizontal" :dataSources="items" :itemSize="110" direction="horizontal" > <template #default="{ source }"> <div class="item-inner-horizontal" :style="{ width: `${source.size}px` }" > <div class="index"># {{ source.index }}</div> <div class="size">{{ source.size }}</div> </div> </template> </FVirtualScroller> </template> <script> const TOTAL_COUNT = 100; const sizes = [60, 80, 100, 150, 180]; const genUniqueId = (prefix) => { return `${prefix}$${Math.random().toString(16).substr(9)}`; }; const dataItems = []; let count = TOTAL_COUNT; while (count--) { const index = TOTAL_COUNT - count; dataItems.push({ index, id: genUniqueId(index), size: sizes[Math.floor(Math.random() * 5)], }); } export default { name: 'Horizontal', setup() { return { items: dataItems, }; }, }; </script> <style scoped> .virtual-scroll-list-horizontal { width: 100%; height: 120px; } .virtual-scroll-list-horizontal .item-inner-horizontal { display: flex; align-items: center; flex-direction: column; padding: 2em 0; } .virtual-scroll-list-horizontal .item-inner-horizontal .index { width: 100%; text-align: center; } .virtual-scroll-list-horizontal .item-inner-horizontal .size { text-align: right; color: darkgray; font-size: 16px; } </style>
无限滚动 
play
<template> <FVirtualScroller ref="virtualList" class="virtual-scroll-list-vertical" :dataSources="dataItems" :itemSize="80" :scrollbarProps="{ height }" @toTop="handleToTop" @toBottom="handleToBottom" > <template #default="{ source, index }"> <div :data-index="index" class="item-inner"> <div class="head"> <span># {{ source.index }}</span> <span>{{ source.name }}</span> </div> <div class="desc">{{ source.desc }}</div> </div> </template> </FVirtualScroller> </template> <script> import { ref } from 'vue'; export default { name: 'Vertical', setup() { const virtualList = ref(null); const height = ref(400); const sentence3 = [ 'BFC(Block formatting context)直译为"块级格式化上下文"。它是一个独立的渲染区域,只有Block-level box参与, 它规定了内部的Block-level Box如何布局,并且与这个区域外部毫不相干。', 'IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)', 'margin 重合,margin 塌陷', 'html5IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)', ]; const genUniqueId = (prefix) => { return `${prefix}$${Math.random().toString(16).substr(9)}`; }; const getSentences = () => { const index = Math.floor(Math.random() * (sentence3.length - 1)); return sentence3[index]; }; const TOTAL_COUNT = 1000; const dataItems = ref([]); const createData = (length, startIndex, isAdd = true) => { const result = []; let count = length; while (count--) { const index = isAdd ? startIndex + length - count - 1 : startIndex - count - 1; result.push({ index, name: `${Math.random()}`, id: genUniqueId(index), desc: getSentences(), }); } return result; }; dataItems.value = createData(TOTAL_COUNT, 1); const handleToBottom = () => { dataItems.value = [...dataItems.value, ...createData(10, dataItems.value[dataItems.value.length - 1].index + 1)]; }; const handleToTop = () => { dataItems.value = [...createData(10, dataItems.value[0].index, false), ...dataItems.value]; setTimeout(() => { virtualList.value.scrollToIndex(10); }, 0); }; return { virtualList, dataItems, height, handleToBottom, handleToTop, }; }, }; </script> <style scoped> .virtual-scroll-list-vertical .item-inner .head { font-weight: 500; } .virtual-scroll-list-vertical .item-inner .head span:first-child { margin-right: 1em; } .virtual-scroll-list-vertical .item-inner .desc { margin-top: 0.5em; margin-bottom: 1em; text-align: justify; } </style>
滚动操作 
play
<template> <FForm :labelWidth="180"> <FFormItem label="触发 toTop 事件阈值:"> <FInputNumber v-model="topThreshold" :min="0" :max="50" :step="10" /> <span style="margin-left: 10px">px</span> </FFormItem> <FFormItem label="触发 toBottom 事件阈值:"> <FInputNumber v-model="bottomThreshold" :min="0" :max="50" :step="10" /> <span style="margin-left: 10px">px</span> </FFormItem> <FSpace> <FButton @click="handleScrollToBottom">滚动到底部位置</FButton> <FButton @click="handleScrollToIndex">滚动到指定索引</FButton> <FButton @click="handleScrollToOffset"> 滚动到相对指定偏移量 </FButton> </FSpace> </FForm> <FDivider /> <FVirtualScroller ref="virtualList" class="virtual-scroll-list-scroll" wrapClass="virtual-scroll-list-wrap" :dataSources="dataItems" :itemSize="100" :scrollbarProps="{ height: 200 }" :topThreshold="topThreshold" :bottomThreshold="bottomThreshold" @scroll="handleScroll" @toTop="handleToTop" @toBottom="handleToBottom" @resized="handleResized" > <template #default="{ source }"> <div class="item-inner"> <div class="head"> <span># {{ source.index }}</span> <span>{{ source.name }}</span> </div> <div class="desc">{{ source.desc }}</div> </div> </template> </FVirtualScroller> <FDivider /> <FSpace vertical> <span>第50项高度: {{ getSize }}</span> <span>当前滚动偏移量: {{ getOffset }}</span> <span>容器高度: {{ getClientSize }}</span> <span>滚动高度: {{ getScrollSize }}</span> </FSpace> </template> <script> import { ref } from 'vue'; import { debounce } from 'lodash-es'; function useDataItems() { // The Climb (From Miley Cyrus) const sentence3 = [ 'BFC(Block formatting context)直译为"块级格式化上下文"。它是一个独立的渲染区域,只有Block-level box参与, 它规定了内部的Block-level Box如何布局,并且与这个区域外部毫不相干。', 'IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)', 'margin 重合,margin 塌陷', 'css3', 'html5IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)', 'es6', ]; const TOTAL_COUNT = 1000; const genUniqueId = (prefix) => { return `${prefix}$${Math.random().toString(16).substr(9)}`; }; const getSentences = () => { const index = Math.floor(Math.random() * (sentence3.length - 1)); return sentence3[index]; }; const dataItems = ref([]); let count = TOTAL_COUNT; while (count--) { const index = TOTAL_COUNT - count; dataItems.value.push({ index, name: `${Math.random()}`, id: genUniqueId(index), desc: getSentences(), }); } return dataItems; } export default { setup() { const virtualList = ref(null); const topThreshold = ref(20); const bottomThreshold = ref(20); const getSize = ref(); const getOffset = ref(); const getClientSize = ref(); const getScrollSize = ref(); const dataItems = useDataItems(); function updateSize() { getSize.value = virtualList.value.getItemSize(49); getOffset.value = virtualList.value.getOffset(); getClientSize.value = virtualList.value.getClientSize(); getScrollSize.value = virtualList.value.getScrollSize(); } const handleScroll = debounce(() => { updateSize(); }, 100); const handleToTop = () => { console.log('[virtualList.scroll] [toTop]'); }; const handleToBottom = () => { console.log('[virtualList.scroll] [toBottom]'); }; const handleResized = debounce((id, size) => { console.log( '[virtualList.scroll] [resized] id:', id, ' size:', size, ); updateSize(); }, 100); const handleScrollToBottom = () => { virtualList.value.scrollToBottom(); }; const handleScrollToIndex = () => { virtualList.value.scrollToIndex(50); }; const handleScrollToOffset = () => { virtualList.value.scrollBy(-50); }; return { virtualList, topThreshold, bottomThreshold, dataItems, handleScroll, handleToTop, handleToBottom, handleResized, handleScrollToBottom, handleScrollToIndex, handleScrollToOffset, getSize, getOffset, getClientSize, getScrollSize, }; }, }; </script> <style> .virtual-scroll-list-scroll .virtual-scroll-list-wrap { width: 1000px; } .virtual-scroll-list-scroll .item-inner .head { font-weight: 500; } .virtual-scroll-list-scroll .item-inner .head span:first-child { margin-right: 1em; } .virtual-scroll-list-scroll .item-inner .desc { margin-top: 0.5em; margin-bottom: 1em; text-align: justify; } </style>
VirtualScroller Props 
| 属性 | 说明 | 类型 | 默认值 | 
|---|---|---|---|
| dataSources | 为列表生成的源数组,每个数组数据必须是一个对象 | Array<Object> | - | 
| keeps | 您期望虚拟列表在真实 dom 中保持渲染的项目数量。 | number | 30 | 
| itemSize | 每项的估计大小,如果它更接近平均大小,滚动条长度看起来更准确。建议指定自己计算的平均值 | number | 50 | 
| itemTag | 子项的包裹元素 | number | div | 
| itemProps | 子项包裹元素的属性 | function | ({ item, index }: { item: ItemData; index: number }) => any | 
| direction | 滚动的方向, 可选值为 vertical 和 horizontal | string | vertical | 
| wrapTag | 列表包裹元素名称 | string | div | 
| wrapClass | 列表包裹元素类名 | string | - | 
| wrapStyle | 列表包裹元素内联样式 | object | {} | 
| topThreshold | 触发toTop 事件的阈值 | number | 0 | 
| bottomThreshold | 触发toBottom 事件的阈值 | number | 0 | 
| scrollbarProps | 滚动条样式,参考滚动条组件 | object | - | 
VirtualScroller Events 
| 事件名称 | 说明 | 回调参数 | 
|---|---|---|
| scroll | 滚动时触发 | (event: Event, range) => void | 
| toTop | 当滚动到顶部或者左边时触发 | () => void | 
| toBottom | 当滚动到底部或者右边时触发,无参数 | () => void | 
VirtualScroller Methods 
| 名称 | 说明 | 参数 | 
|---|---|---|
| scrollRef | 滚动条 | - | 
| scrollToIndex | 手动将滚动位置设置为指定索引 | () => void | 
| scrollToBottom | 手动将滚动位置设置到最底部 | (index: number) => void | 
| scrollBy | 手动将滚动位置设置为相对指定偏移量 | (offset: number) => void | 
| scrollTo | 手动将滚动位置设置为指定偏移量 | (offset: number) => void | 
| getItemOffset | 获取选项位置 | (index: number) => number | 
| getItemSize | 获取选项大小 | (index: number) => number | 
| getOffset | 获取当前滚动偏移量 | () => number | 
| getClientSize | 获取包装器元素客户端视口大小(宽度或高度) | () => number | 
| getScrollSize | 获取所有滚动大小(滚动高度或滚动宽度) | () => number | 
| findStartIndex | 获取显示的初始元素索引 | () => number | 
| findEndIndex | 获取显示的结束元素索引 | () => number |