虚拟列表:简而言之,对于大量列表数据,仅渲染滚动区域内可视区域 所对应 数据源 的部分数据
介绍
举个例子:
比如我要渲染 1000 条数据,每条数据高度 60px,如果我想把全部元素渲染,那么滚动区域至少需要 6000px 高但是呢,如果我渲染 1000 条数据,那么整个 dom 树就会出现巨量 dom 元素,如下图,B,C,D 区域将全部都是 dom 元素,一旦回流重绘就会非常卡顿。
这种场景的优化方案就是采取虚拟列表去渲染,即我们要做的就是在滚动的时候只渲染可视区域对应的 dom 元素,即下图的 B,非可视区域内元素不去渲染(即 C,D 区域不渲染元素),这样的话可以减少大量非必要 DOM 渲染(C,D 区域用户也不需要看见,所以它的 dom 渲染是非必要的),去提高性能!
实现原理
根据视口高度(上图中的 B 区域高度),父元素的滚动距离(scrollTop)及每一个列表元素 item 的平均高度,计算出当前 scrollTop 到 scrollTop+视口高度这段可视区域 对应数据源列表所需要展示的 部分数据,再将这部分数据展示在 B 区域内,对于隐藏滚动区域 C,D 使用 paddingTop&&paddingBottom 填充,保持整个滚动区域高度不变,同时监听滚动,滚动触发时动态改变可是区域内数据及 paddingTop&&paddingBottom 等数据
- 1,监听容器元素滚动
- 2,获取容器元素滚动距离 scrollTop
- 3,获取当前滚动距离(scrollTop)下可视区域首个需要渲染元素索引(in 数据源)startIndex,计算方式:startIndex = 滚动距离(scrollTop)/ 列表元素高度(itemHeight)
- 4,获取当前滚动距离(scrollTop)下可视区域最后一个需要渲染元素索引(in 数据源)endIndex,计算方式:endIndex = startIndex + 可视区域高度(viewHeight)/ 列表元素高度(itemHeight)
- 5,根据 startIndex 和 endIndex 获取数据源对应的 可视区域需要展示的数据 viewData,计算方式:viewData =dataSource(数据源).slice(startIndex,endIndex+1)
- 6,计算 C 区域的高度,既 paddingTop,计算方式:paddingTop = startIndex*itemHeight(列表元素高度)
- 7,计算 D 区域的高度,既 paddingBottom,计算方式:paddingBottom = contentHeight(整个滚动区域高度,既 A 区域)-paddingTop-可视区域高度(viewHeight)
- 8,清除可是区域内上一次展示的数据,使用新数据 viewData 重新填充,且设置新的 paddingTop&paddingBottom
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Document</title> <style> html, body { width: 100%; height: 100%; margin: 0; }
#container { width: 100%; height: 100%; }
#content { background-color: pink; } </style> </head>
<body> <div id="container"> <div id="content"></div> </div> <script> function createVlist(container, content, data_Source, item_Height) { const dataSource = data_Source const itemHeight = item_Height const viewHeight = container.clientHeight const contentHeight = itemHeight * dataSource.length const itemCount = Math.ceil(viewHeight / itemHeight) container.setAttribute('style', `overflow:auto`) content.setAttribute('style', `height:${contentHeight}px`) const scrollCallback = (e) => { const scrollTop = e.target.scrollTop const startIndex = Math.ceil(scrollTop / itemHeight) const endIndex = startIndex + itemCount const itemList = dataSource.slice(startIndex, endIndex + 1)
const paddingTop = startIndex * itemHeight const paddingBottom = contentHeight - paddingTop - itemCount * itemHeight
content.setAttribute( 'style', `padding-top:${paddingTop}px;padding-bottom:${paddingBottom}px` ) content.innerHTML = '' for (const val of itemList) { const item = document.createElement('div') item.innerHTML = val item.setAttribute( 'style', `background-color:${ val % 2 === 0 ? 'red' : 'blue' };width:100%;height:${item_Height}px` ) content.appendChild(item) } } container.addEventListener('scroll', scrollCallback) scrollCallback({ target: container }) } createVlist( container, content, Array.from({ length: 100 }, (v, i) => i), 60 ) </script> </body> </html>
|
Vue 使用虚拟列表
1
| npm i vue-virtual-scroller
|
在main.js
中引入
1 2 3
| import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' import vuevirtualscroller from 'vue-virtual-scroller' Vue.use(vuevirtualscroller)
|
基础用法(固定大小)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| <template> <RecycleScroller class="scroller" :items="list" :item-size="32" key-field="id" v-slot="{ item }"> <div class="user"> {{ item.name }} </div> </RecycleScroller> </template>
<script> export default { name: 'Home', components: {}, data() { return { items: [], } }, created() { this.getData() }, methods: { getData() { for (let i = 0; i < 200000; i++) { this.items.push({ title: 'ssadqwesadwqe', id: i }) } }, }, } </script>
<style lang="less" scoped> .scroller { height: 300px; background-color: rgba(0, 0, 0, 0.1); }
.user { height: 32%; padding: 0 12px; display: flex; align-items: center; } </style>
|
使用<RecycleScroller>
,items
绑定数据源,item-size
设置的子元素的高度,父子元素高度都必须固定
上拉加载(固定大小)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| <template> <div> <RecycleScroller class="scroller" :items="items" :item-size="150" :emitUpdate="true" @update="update" v-if="items.length"> <template slot-scope="props"> <li :key="props.itemKey"> <div>{{ props.item.title }}</div> <img :src="props.item.img" alt="" /> </li> </template> </RecycleScroller> </div> </template> <script> export default { name: 'test', data() { return { items: [], } }, created() { this.getData() }, mounted() {}, methods: { getData() { let arr = [] for (let i = 1; i < 20; i++) { arr.push({ id: i + 1, img: 'http://dummyimage.com/200x100/FF6600', time: '2003-02-02', title: 'hahahha', }) } this.items = arr }, update(start, end) { if (end === this.items.length) { let temp = [] let id = this.items[this.items.length - 1].id let arr = [] for (let i = 1; i < 20; i++) { arr.push({ id: id + i, img: 'http://dummyimage.com/200x100/FF6600', time: '2003-02-02', title: 'hahahha', }) } let res = { code: 200, data: arr, } temp = [...this.items, ...res.data] this.items = temp } }, }, } </script> <style lang="css" scoped> .scroller { height: 300px; background-color: #ccc; }
.user { height: 150px; padding: 0 12px; display: flex; align-items: center; } </style>
|
加入:emitUpdate=“true”
和@update=“update”
,emitUpdate 必须设置为 true,update
中定义数据更新的方法
上拉加载(可变大小)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| <template> <div> <DynamicScroller :items="items" :min-item-size="54" class="scroller" :emitUpdate="true" @update="update" v-if="items.length"> <template v-slot="{ item, index, active }"> <DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.message]" :data-index="index"> <li class="single-item" :key="item.id"> <div class="right-info"> <span>标题:{{ item.title }}</span> <span>项目数量:{{ item.id }}</span> <span>项目时间:{{ item.time }}</span> <span>项目描述:{{ item.des }}</span> </div> </li> </DynamicScrollerItem> </template> </DynamicScroller> </div> </template> <script> export default { name: 'test', data() { return { items: [], } }, created() { this.getData() }, mounted() {}, methods: { getData() { let arr = [] for (let i = 1; i < 20; i++) { arr.push({ id: i + 1, img: 'http://dummyimage.com/200x100/FF6600', time: '2003-02-02', title: 'hahahha', }) } this.items = arr }, update(start, end) { if (end === this.items.length) { let temp = [] let id = this.items[this.items.length - 1].id let arr = [] for (let i = 1; i < 20; i++) { arr.push({ id: id + i, img: 'http://dummyimage.com/200x100/FF6600', time: '2003-02-02', title: 'hahahha', }) } let res = { code: 200, data: arr, } temp = [...this.items, ...res.data] this.items = temp } }, }, } </script> <style lang="less" scoped> .scroller { height: calc(100vh - 3rem); background-color: #ccc; }
.user { height: 32%; padding: 0 12px; display: flex; } .single-item { display: flex; justify-content: flex-start; align-items: center; border-bottom: 1px solid rgb(187, 167, 167); .left-pic { width: 200px; img { width: 200px; } } .right-info { padding-left: 20px; text-align: left; span { display: block; &:last-child { word-break: break-all; } } } } </style>
|
引用站外地址
vue-virtual-scroller 官方文档
超快滚动任意数量的数据
参数
参数 |
说明 |
默认值 |
items |
要在滚动条中显示的项目列表。 |
- |
direction |
滚动方向(vertical 或horizontal ) |
vertical |
itemSize |
以像素为单位显示项目的高度(或水平模式下的宽度),用于计算滚动大小和位置。 如果设置为(默认值),它将使用可变大小模式。 |
null |
gridItems |
在同一行上显示多个项目以创建网格。您必须输入一个值才能使用此道具(不支持动态大小)。 |
- |
itemSecondarySize |
设置时网格中项目的大小(垂直模式下的宽度,水平模式下的高度)。 如果未设置,它将使用 gridItems itemSecondarySize itemSize 的值。 |
- |
minItemSize |
如果项目的高度(或水平模式下的宽度)未知,则使用的最小大小。 |
- |
sizeField |
用于在可变大小模式下获取项目大小的字段。 |
size |
typeField |
用于区分列表中不同类型的组件的字段。对于每种不同的类型,将创建一个回收物品池。 |
type |
keyField |
用于标识项目和优化管理渲染视图的字段。 |
id |
pageMode |
启用页面模式。 |
false |
prerender |
为服务器端呈现 (SSR) 渲染固定数量的项目。 |
0 |
buffer |
要添加到滚动可见区域边缘以开始渲染更远的项目的像素量。 |
200 |
emitUpdate |
每次更新虚拟滚动条内容时发出一个事件(可能会影响性能)。 |
false |
listClass |
添加到项目列表包装器的自定义类。 |
- |
itemClass |
添加到每个项目的自定义类。 |
- |
listTag |
要呈现为列表包装器的元素。 |
div |
itemTag |
要呈现为列表项(默认槽内容的直接父级)的元素。 |
div |
事件
事件名 |
说明 |
参数 |
resize |
当滚动条的大小更改时发出。 |
|
visible |
当滚动条认为自己在页面中可见时发出。 |
|
hidden |
当滚动条隐藏在页面中时发出。 |
|
update |
每次更新视图时发出。emitUpdate 需设置为 true |
startIndex, endIndex, visibleStartIndex, visibleEndIndex |
scroll-start |
渲染第一项时发出。 |
|
scroll-end |
呈现最后一项时发出。 |
|