前言

在工作中,有时会遇到需要一些不能使用分页方式来加载列表数据的业务情况,对于此,我们称这种列表叫做长列表。比如,在一些外汇交易系统中,前端会实时的展示用户的持仓情况(收益、亏损、手数等),此时对于用户的持仓列表一般是不能分页的。

在高性能渲染十万条数据(时间分片)一文中,提到了可以使用时间分片的方式来对长列表进行渲染,但这种方式更适用于列表项的DOM结构十分简单的情况。本文会介绍使用虚拟列表的方式,来同时加载大量数据。

为什么需要使用虚拟列表

假设我们的长列表需要展示10000条记录,我们同时将10000条记录渲染到页面中,先来看看需要花费多长时间:

button

document.getElementById('button').addEventListener('click',function(){//记录任务开始时间

let now =Date.now();//插入一万条数据

const total = 10000;//获取容器

let ul = document.getElementById('container');//将数据插入容器中

for (let i = 0; i < total; i++) {

let li= document.createElement('li');

li.innerText= ~~(Math.random() *total)

ul.appendChild(li);

}

console.log('JS运行时间:',Date.now() -now);

setTimeout(()=>{

console.log('总运行时间:',Date.now() -now);

},0)//print JS运行时间: 38

//print 总运行时间: 957

})

当我们点击按钮,会同时向页面中加入一万条记录,通过控制台的输出,我们可以粗略的统计到,JS的运行时间为38ms,但渲染完成后的总时间为957ms。

简单说明一下,为何两次console.log的结果时间差异巨大,并且是如何简单来统计JS运行时间和总渲染时间:

在 JS 的Event Loop中,当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完后,才会触发渲染线程对页面进行渲染

第一个console.log的触发时间是在页面进行渲染之前,此时得到的间隔时间为JS运行所需要的时间

第二个console.log是放到 setTimeout 中的,它的触发时间是在渲染完成,在下一次Event Loop中执行的

关于Event Loop的详细内容请参见这篇文章-->

然后,我们通过Chrome的Performance工具来详细的分析这段代码的性能瓶颈在哪里:

从Performance可以看出,代码从执行到渲染结束,共消耗了960.8ms,其中的主要时间消耗如下:

Event(click) : 40.84ms

Recalculate Style : 105.08ms

Layout : 731.56ms

Update Layer Tree : 58.87ms

Paint : 15.32ms

从这里我们可以看出,我们的代码的执行过程中,消耗时间最多的两个阶段是Recalculate Style和Layout。

Recalculate Style:样式计算,浏览器根据css选择器计算哪些元素应该应用哪些规则,确定每个元素具体的样式。

Layout:布局,知道元素应用哪些规则之后,浏览器开始计算它要占据的空间大小及其在屏幕的位置。

在实际的工作中,列表项必然不会像例子中仅仅只由一个li标签组成,必然是由复杂DOM节点组成的。

那么可以想象的是,当列表项数过多并且列表项结构复杂的时候,同时渲染时,会在Recalculate Style和Layout阶段消耗大量的时间。

而虚拟列表就是解决这一问题的一种实现。

什么是虚拟列表

虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。

假设有1万条记录需要同时渲染,我们屏幕的可见区域的高度为500px,而列表项的高度为50px,则此时我们在屏幕中最多只能看到10个列表项,那么在首次渲染的时候,我们只需加载10条即可。

说完首次加载,再分析一下当滚动发生时,我们可以通过计算当前滚动值得知此时在屏幕可见区域应该显示的列表项。

假设滚动发生,滚动条距顶部的位置为150px,则我们可得知在可见区域内的列表项为第4项至`第13项。

实现

虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。

计算当前可视区域起始数据索引(startIndex)

计算当前可视区域结束数据索引(endIndex)

计算当前可视区域的数据,并渲染到页面中

计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上

由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:

infinite-list-container 为可视区域的容器

infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条

infinite-list 为列表项的渲染区域

接着,监听infinite-list-container的scroll事件,获取滚动位置scrollTop

假定可视区域高度固定,称之为screenHeight

假定列表每项高度固定,称之为itemSize

假定列表数据称之为listData

假定当前滚动位置称之为scrollTop

则可推算出:

列表总高度listHeight = listData.length * itemSize

可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)

数据的起始索引startIndex = Math.floor(scrollTop / itemSize)

数据的结束索引endIndex = startIndex + visibleCount

列表显示数据为visibleData = listData.slice(startIndex,endIndex)

当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。

偏移量startOffset = scrollTop - (scrollTop % itemSize);

最终的简易代码如下:

>{{ item.value }}

exportdefault{

name:'VirtualList',

props: {//所有列表数据

listData:{

type:Array,default:()=>[]

},//每项高度

itemSize: {

type: Number,default:200}

},

computed:{//列表总高度

listHeight(){return this.listData.length * this.itemSize;

},//可显示的列表项数

visibleCount(){return Math.ceil(this.screenHeight / this.itemSize)

},//偏移量对应的style

getTransform(){return `translate3d(0,${this.startOffset}px,0)`;

},//获取真实显示列表数据

visibleData(){return this.listData.slice(this.start, Math.min(this.end,this.listData.length));

}

},

mounted() {this.screenHeight = this.$el.clientHeight;this.start = 0;this.end = this.start + this.visibleCount;

},

data() {return{//可视区域高度

screenHeight:0,//偏移量

startOffset:0,//起始索引

start:0,//结束索引

end:null,

};

},

methods: {

scrollEvent() {//当前滚动位置

let scrollTop = this.$refs.list.scrollTop;//此时的开始索引

this.start = Math.floor(scrollTop / this.itemSize);//此时的结束索引

this.end = this.start + this.visibleCount;//此时的偏移量

this.startOffset = scrollTop - (scrollTop % this.itemSize);

}

}

};

点击查看在线DEMO及完整代码

最终效果如下:

列表项动态高度

在之前的实现中,列表项的高度是固定的,因为高度固定,所以可以很轻易的获取列表项的整体高度以及滚动时的显示数据与对应的偏移量。而实际应用的时候,当列表中包含文本之类的可变内容,会导致列表项的高度并不相同。

比如这种情况:

在虚拟列表中应用动态高度的解决方案一般有如下三种:

1.对组件属性itemSize进行扩展,支持传递类型为数字、数组、函数

可以是一个固定值,如 100,此时列表项是固高的

可以是一个包含所有列表项高度的数据,如 [50, 20, 100, 80, ...]

可以是一个根据列表项索引返回其高度的函数:(index: number): number

这种方式虽然有比较好的灵活度,但仅适用于可以预先知道或可以通过计算得知列表项高度的情况,依然无法解决列表项高度由内容撑开的情况。

2.将列表项渲染到屏幕外,对其高度进行测量并缓存,然后再将其渲染至可视区域内。

由于预先渲染至屏幕外,再渲染至屏幕内,这导致渲染成本增加一倍,这对于数百万用户在低端移动设备上使用的产品来说是不切实际的。

3.以预估高度先行渲染,然后获取真实高度并缓存。

这是我选择的实现方式,可以避免前两种方案的不足。

接下来,来看如何简易的实现:

定义组件属性estimatedItemSize,用于接收预估高度

props: {//预估高度

estimatedItemSize:{

type:Number

}

}

定义positions,用于列表项渲染后存储每一项的高度以及位置信息,

this.positions =[//{

//top:0,

//bottom:100,

//height:100

//}

];

并在初始时根据estimatedItemSize对positions进行初始化。

initPositions(){this.positions = this.listData.map((item,index)=>{return{

index,

height:this.estimatedItemSize,

top:index* this.estimatedItemSize,

bottom:(index+ 1) * this.estimatedItemSize

}

})

}

由于列表项高度不定,并且我们维护了positions,用于记录每一项的位置,而列表高度实际就等于列表中最后一项的底部距离列表顶部的位置。

//列表总高度

listHeight(){return this.positions[this.positions.length - 1].bottom;

}

由于需要在渲染完成后,获取列表每项的位置信息并缓存,所以使用钩子函数updated来实现:

updated(){

let nodes= this.$refs.items;

nodes.forEach((node)=>{

let rect=node.getBoundingClientRect();

let height=rect.height;

let index= +node.id.slice(1)

let oldHeight= this.positions[index].height;

let dValue= oldHeight -height;//存在差值

if(dValue){this.positions[index].bottom = this.positions[index].bottom -dValue;this.positions[index].height =height;for(let k = index + 1;k

}

}

})

}

滚动后获取列表开始索引的方法修改为通过缓存获取:

//获取列表起始索引

getStartIndex(scrollTop = 0){

let item= this.positions.find(i => i && i.bottom >scrollTop);returnitem.index;

}

由于我们的缓存数据,本身就是有顺序的,所以获取开始索引的方法可以考虑通过二分查找的方式来降低检索次数:

//获取列表起始索引

getStartIndex(scrollTop = 0){//二分法查找

return this.binarySearch(this.positions,scrollTop)

},//二分法查找

binarySearch(list,value){

let start= 0;

let end= list.length - 1;

let tempIndex= null;while(start <=end){

let midIndex= parseInt((start + end)/2);

let midValue =list[midIndex].bottom;if(midValue ===value){return midIndex + 1;

}else if(midValue

start= midIndex + 1;

}else if(midValue >value){if(tempIndex === null || tempIndex >midIndex){

tempIndex=midIndex;

}

end= end - 1;

}

}returntempIndex;

},

滚动后将偏移量的获取方式变更:

scrollEvent() {//...省略

if(this.start >= 1){this.startOffset = this.positions[this.start - 1].bottom

}else{this.startOffset = 0;

}

}

通过faker.js 来创建一些随机数据

let data =[];for (let id = 0; id < 10000; id++) {

data.push({

id,

value: faker.lorem.sentences()//长文本

})

}

点击查看在线DEMO及完整代码

最终效果如下:

从演示效果上看,我们实现了基于文字内容动态撑高列表项情况下的虚拟列表,但是我们可能会发现,当滚动过快时,会出现短暂的白屏现象。

为了使页面平滑滚动,我们还需要在可见区域的上方和下方渲染额外的项目,在滚动时给予一些缓冲,所以将屏幕分为三个区域:

可视区域上方:above

可视区域:screen

可视区域下方:below

定义组件属性bufferScale,用于接收缓冲区数据与可视区数据的比例

props: {//缓冲区比例

bufferScale:{

type:Number,default:1}

}

可视区上方渲染条数aboveCount获取方式如下:

aboveCount(){return Math.min(this.start,this.bufferScale * this.visibleCount)

}

可视区下方渲染条数belowCount获取方式如下:

belowCount(){return Math.min(this.listData.length - this.end,this.bufferScale * this.visibleCount);

}

真实渲染数据visibleData获取方式如下:

visibleData(){

let start= this.start - this.aboveCount;

let end= this.end + this.belowCount;return this._listData.slice(start, end);

}

点击查看在线DEMO及完整代码

最终效果如下:

基于这个方案,个人开发了一个基于Vue2.x的虚拟列表组件:vue-virtual-listview,可点击查看完整代码。

面向未来

在前文中我们使用监听scroll事件的方式来触发可视区域中数据的更新,当滚动发生后,scroll事件会频繁触发,很多时候会造成重复计算的问题,从性能上来说无疑存在浪费的情况。

可以使用IntersectionObserver替换监听scroll事件,IntersectionObserver可以监听目标元素是否出现在可视区域内,在监听的回调事件中执行可视区域数据的更新,并且IntersectionObserver的监听回调是异步触发,不随着目标元素的滚动而触发,性能消耗极低。

遗留问题

我们虽然实现了根据列表项动态高度下的虚拟列表,但如果列表项中包含图片,并且列表高度由图片撑开,由于图片会发送网络请求,此时无法保证我们在获取列表项真实高度时图片是否已经加载完成,从而造成计算不准确的情况。

这种情况下,如果我们能监听列表项的大小变化就能获取其真正的高度了。我们可以使用ResizeObserver来监听列表项内容区域的高度改变,从而实时获取每一列表项的高度。

不过遗憾的是,在撰写本文的时候,仅有少数浏览器支持ResizeObserver。

转发来源:作者:云中桥 链接:https://juejin.im/post/5db684ddf265da4d495c40e5

喜欢这篇文章?欢迎打赏~~

mysql虚拟列表_「前端进阶」高性能渲染十万条数据(虚拟列表)相关推荐

  1. 在vue中如何高性能渲染十万条数据(虚拟列表)并且增加个搜索框可以搜索到这些数据

    在工作中,有时会遇到需要一些不能使用分页方式来加载列表数据的业务情况,对于此,我们称这种列表叫做长列表.比如,在一些外汇交易系统中,前端会实时的展示用户的持仓情况(收益.亏损.手数等),此时对于用户的 ...

  2. class react 获取_「前端进阶」React系列九 - 受控非受控组件

    源自:coderwhy 一. refs的使用 在React的开发模式中,通常情况下不需要.也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作: 管理焦点,文本选择或媒体播 ...

  3. dreamweaver 正则表达式为属性值加上双引号_「前端篇」不再为正则烦恼

    作者:李一二 转发链接:https://mp.weixin.qq.com/s/PmzEbyFQ8FynIlXuUL0H-g 前言 有不少朋友都为写正则而头疼,不过笔者早已不为正则而烦恼了.本文分享一些 ...

  4. mysql 插入秒_教你88秒插入1000万条数据到mysql数据库表,IG牛逼

    我用到的数据库为,mysql数据库5.7版本的首先自己准备好数据库表 其实我在插入1000万条数据的时候遇到了一些问题,现在先来解决他们,一开始我插入100万条数据时候报错,控制台的信息如下: com ...

  5. mysql数据字典生成器_「python技术」列表推导、生成器表达式和字典推导的差异及其示例

    #开往春天新创作大赛# 前言 列表推导式构建列表的快捷方式,而生成器表达式则可以用来创建其他任何类型的序列.自python2.7以后,列表推导和生成器表达式的概念就应用到了字典上,所以就有了字典推导, ...

  6. file 选择的文件胖多有多大_「HTML5 进阶」FileAPI 文件操作实战,内附详细案例,建议收藏...

    FileAPI 介绍 HTML5 为我们提供了 File API 相关规范.主要涉及 File 接口 和 FileReader 对象 . 本文整理了兼容性检测.文件选择.属性读取.文件读取.进度监控. ...

  7. vue:无法将“vue”识别为脚本_「前端架构」React和Vue -CTO的选择正确框架的指南...

    快速总结:为项目选择正确的javascript框架或库是CTO和项目经理的基本任务.然而,选择的范围很大程度上取决于几个因素,如项目时间.学习曲线.框架性能和团队规模.这篇文章旨在指导他们选择正确的j ...

  8. 「前端进阶」vue+koa2+mongodb+oss阿里云部署自己的网站

    线上 线上地址:itc 项目地址:GITHUB 由于使用Element框架实现前端,并没有做移动端适配,所以建议PC端观看. 建议参考本文最后教程,将项目克隆到本地启动(不需要任何云系列产品即可本地启 ...

  9. 分类型变量预测连续型变量_「JS进阶」你真的掌握变量和类型了吗

    文章转载自公众号 code秘密花园 , 作者 ConardLi 一起看下面几个问题: JavaScript中的变量在内存中的具体存储形式是什么? 0.1+0.2为什么不等于0.3?发生小数计算错误的具 ...

最新文章

  1. python3中的推导式、生成器、迭代器
  2. postfix 554-5.7.0 Reject
  3. golang beego orm报错 must have one register DataBase alias named `default` 解决方案
  4. word怎么设置图片编号(图片下标,图片标签,图片序号,图片注释,题注)(交叉引用)
  5. 转python version 2.7 required,which was not found in the registry
  6. Flask部署| gunicorn、nginx部署flask项目,并用supervisor来管理进程
  7. python常用库有哪些餐厅_Python常用库整理
  8. [css] 说说你对低版本IE的盒子模型的理解
  9. [Leetcode][第95题][JAVA][不同的二叉搜索树 II][递归]
  10. docx文档怎么排列图片_“胶水语言”办公自动化Word篇——使用Python编辑和读取Word文档
  11. Hadoop-2.2.0中文文档—— Common - CLI MiniCluster
  12. 基于javaweb的本科生实习管理系统
  13. pytorch加载模型指定GPU卡号报错或者指定失败
  14. HR 必知的 360 评估
  15. ubuntu搭建PHP网站完整实例教程
  16. 图神经网络七日打卡营 Day 01 什么是非欧空间
  17. [html代码] 几种美丽的分割线
  18. python计算分位数方法
  19. Meta-Learning之How to train your MAML
  20. 黑马在线教育数仓实战2

热门文章

  1. Win2008配置终端服务网络负载平衡实战 -2
  2. opencv获取mat的指针_数字图像处理之opencv中Mat数据操作
  3. 信息学奥赛一本通(1245:不重复地输出数)
  4. 信息学奥赛一本通(1024:保留3位小数的浮点数)
  5. 垂直柱状图(洛谷-P1598 )
  6. 计算2的幂(信息学奥赛一本通-T1037)
  7. 44 SD配置-销售凭证设置-定义状态参数文件
  8. 20 CO配置-控制-产品成本控制-产品成本计划编制-定义成本核算变式
  9. 11 所允许的仓储单位类型没有针对仓储类型xxx定义
  10. 云计算交流会计算机操作,计算机二级考试真题-word-小王-云计算技术交流大会...