![3b3f35e13849eac35c0d4644b4a1475a.png](https://i-blog.csdnimg.cn/blog_migrate/910de860a59186c831ad4f7fad83678e.png)
背景:系统中有一个添加品牌的搜索框,当搜索类目不做限制的时候,全部的品牌列表会有1W多个,这时候在框架的加持下,操作速度感人。可以在https://codesandbox.io/s/pure-vue-kqber中体验一下,甚至不用打开控制台看console输出,就可以感受到载入长列表和重置之间切换时,页面停止响应的时间。
问题产生原因
DOM节点数量过多,浏览器渲染吃力
![1876afc3b2dd10d936cfa1fc562e5b89.png](https://i-blog.csdnimg.cn/blog_migrate/03cffa99aff449f344dca7bf7cdae0f0.png)
其实不光是初次渲染时间长,如果有大量节点出现,那么在滚动的时候,也能明显感受到不流畅的滚动现象。
可选方案
懒加载
通过懒加载的方式,在出现长列表的时候,第一次并不完全渲染所有的DOM节点,即可解决一部分场景下的问题。
优点:实现简单
缺点:
- 想要定位到某一个位置的数据会非常困难
- 如果一直加载到底,那么最终还是会出现大量的DOM节点,导致滚动不流畅
虚拟渲染
懒加载无法满足真正的长列表展示,那么如果真正要解决此类问题该怎么办?还有一种思路就是:列表局部渲染,又被称为虚拟列表.
当前比较知名的一些第三方库有vue-virtual-scroller、react-tiny-virtual-list、react-virtualized。它们都可以利用局部加载解决列表过长的问题的,vue-virtual-scroller、react-tiny-virtual-list一类的方案只支持虚拟列表,而react-virtualized这种大而全的库则是支持表格、集合、列表等多种情况下的局部加载方案。
单纯列表虚拟渲染
我们先看下vue-virtual-scroller、react-tiny-virtual-list这种纯虚拟列表的解决方案。它们的实现原理是利用视差和错觉制作一份出一份“虚拟”列表,一个虚拟列表由三部分组成:
- 视窗口
- 虚拟数据列表(数据展示)
- 滚动占位区块(底部滚动区)
虚拟列表侧面图示意:
![a18c895dc825afc6998afdb502831de3.png](https://i-blog.csdnimg.cn/blog_migrate/6f174929f89acbc43f44b1ecf7f8d171.jpeg)
正面图:
![f3867844533ad808b7b4192e4b22533d.png](https://i-blog.csdnimg.cn/blog_migrate/7fd0af6a07c6316aa95b6b39e1469087.png)
滚动一段距离后:
![d2181e9a7659186ec35d081c4eb75989.png](https://i-blog.csdnimg.cn/blog_migrate/e7a5bc1e22bc21423d3bbf36609565c7.png)
最终要实现的效果:由滚动占位区块产生滚动条,随着滚动条的移动,在可视窗口展示虚拟数据列表
react-virtualized的二维虚拟渲染
react-virtualized的实现方案和我们上面探讨的不太一样,因为表格是二维的,而列表是一维的(可以认为列表是一种特殊的表格),react-virtualized就是在二维的基础上构建的一套虚拟数据渲染工具。
示意图如下:
![aa67b7c2c306ceddd36eb871804d3ff6.png](https://i-blog.csdnimg.cn/blog_migrate/6f9ec06dc73ee2889abdbda1188548e7.png)
蓝色的部分被称为Cell,上面白色线分隔的区块叫做Section。
基本原理:在列表的上方打上一层方格(Section),下面的每个元素(Cell)都能落到某个方格上(Section)。滚动的时候,随着Cell的动态增加,Section也会被动态的创建,将每一个Cell都注册到对应的Section下。根据当前滚动到的Section,可以得到当前Section下包含的Cell,从此将Cell渲染出来。
/*
0 1 2 3 4 5
┏━━━┯━━━┯━━━┓
0┃0 0┊1 3┊6 6┃
1┃0 0┊2 3┊6 6┃
┠┈┈┈┼┈┈┈┼┈┈┈┨
2┃4 4┊4 3┊7 8┃
3┃4 4┊4 5┊9 9┃
┗━━━┷━━━┷━━━┛
Sections to Cells map:
0.0 [0]
1.0 [1, 2, 3]
2.0 [6]
0.1 [4]
1.1 [3, 4, 5]
2.1 [7, 8, 9]
*/
本次实现方案
由于我们的目的是处理前端超长列表,而react-virtualized的实现方案是基于二维表格的,其List组件也是继承自Grid组件,如果要做列表方案,必须先实现二维的Grid方案。只处理长列表的情况下,实现一个单纯的虚拟列表渲染方案比二维的Grid方案要更合适一些。
基本结构
首先我们按照虚拟列表示意图来规划出若干个元素。.virtual-scroller乃整个滚动列表组件,在最外层监测其滚动事件。在内部我们需放置一个.phantom来撑开容器,使滚动条出现,并且该元素的高度 = 数据总数 * 列表项高度。接着我们在.phantom的上一层,再画出一个ul列表,它被用来动态加载数据,而它的位置和数据将由计算得出。
https://codesandbox.io/s/list--scrollbar-basic-bbxlq
<template>
<div id="app">
<input type="text" v-model.number="dataLength">条
<div class="virtual-scroller" @scroll="onScroll" :style="{height: 600 + 'px'}">
<div class="phantom" :style="{height: this.dataLength * itemHeight + 'px'}">
<ul :style="{'margin-top': `${scrollTop}px`}">
<li v-for="item in visibleList" :key="item.brandId" :style="{height: `${itemHeight}px`, 'line-height': `${itemHeight}px`}">
<div>
<div>{
{item.name}}</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
itemHeight: 60,
visibleCount: 10,
dataLength: 100,
startIndex: 0,
endIndex: 10,
scrollTop: 0
};
},
computed: {
dataList() {
const newDataList = [...Array(this.dataLength || 0).keys()].map((v, i) => ({
brandId: i + 1,
name: `第${
i + 1}项`,
height: this.itemHeight
}));
return newDataList;
},
visibleList() {
return this.dataList.slice(this.startIndex, this.endIndex);
}
},
watch: {
dataList() {
console.time('rerender&#