最终效果
前言
恰逢周末~ 早上起床,拥抱过☀️太阳,于是开始做家庭作业~
众所周知的一道面试题:如何处理后端返回的10w+数据 ? 对于这种问题很多人一听就想抡起锤子?,扬言锤爆后端,但是还别说,真的会有这样场景,如处理Excel表格、做大数据分析,对数据库数据进行管理,不说有10w+数据,但是若前端直接渲染,这种量级还是足以让浏览器卡顿,直至卡死(可以看我源码中的.csv文件)。
因此面对这个问题应该从几个方面思考:
-
服务端的问题,优先从服务端着手,后端能否对数据进行分页呢?
-
从服务端思考,后端若不处理,前端能否做个node中间层去处理这些数据?
-
最后,前端只能祭出杀手锏,虚拟滚动!
什么是虚拟滚动
场景
通常,在H5中,列表页的展示可谓家常便饭,如果列表数据量很大又无限下拉,这时候大量dom的渲染将导致页面卡顿;而在PC端上,若是大数据往往都是通过表格进行渲染,或者一个大的tree进行渲染,例如使用Element UI 的 tree组件,一旦需要渲染量级稍微多点的数据,tree组件会递归的生成dom然后渲染到页面,从而导致页面的卡顿。
特点
知道了场景,自然就衍生出了较为通用的解决方案——虚拟滚动,虚拟滚动就是只渲染当前视口的dom元素(当然也可以渲染3页作为buff缓存,让用户体验更好~),虚拟滚动核心就是按需渲染,不在可视区域的元素不需要渲染,因此也叫可视区域渲染。特点如下:
- 滚动容器:像window窗口就是可以滚的,也可通过
overflow
通过布局的方式实现滚动,从而通过onScroll
事件对容器里面的元素进行滚动监听,就可知晓元素的相对于容器的位置。 - 可滚动区域: 容器内部可以滚动的区域是多少,比如有1000个元素,
width
是60,那么可滚动的区域就是 1000*60 px。
- 可视区域: 看得见的区域,比如H5中屏幕大小,浏览器的视觉窗口大小。
虚拟滚动实现
因此虚拟滚动的原理就是:当用户在可滚动区域滑动时,容器可通过监听知道scrollTop
、scrollLeft
的变化,从而可以知道当前那些元素应该渲染到可视区域。
则实现一个支持横向虚拟滚动的表格组件的思路如下:
- 计算可视区域的
width
; - 依据
scrollLeft
推算出可视区域显示哪几列;
- 在滚动时,实时监听滚动计算第2步;
- 计算出表格内容的总宽度,通过
transform
隐藏未渲染部分
直接上代码:github.com/AutumnWhj/v…
表格组件—横向虚拟滚动实现
明确最终调用
表格可分为表头跟表格主体的数据部分,即header跟body,因此表格组件的接口可以如下:里面根据数据去实现自有逻辑。
<acho-virtual-table
:columns="columns"
:dataSource="dataSource"
/>
复制代码
可滚动table布局
按虚拟滚动的特点,可以做出以下布局,布局合理了就成功了一大半,后面需要做的无法就是对滚动时的监听做些许的计算。
计算窗口可渲染的列-columns
要计算可视窗口的列,得依据scrollLeft
,即相对滚动容器,窗口往右滚动的距离去计算哪一列可以被看到,如下:
handleBodyScrollLeft(event) {
const scrollLeft = event.target.scrollLeft
let start = Math.floor(scrollLeft / this.itemWidth)
this.start = Math.max(start, 0)
//this.visibleCount = Math.max(Math.ceil(width / this.itemWidth), 10)
this.end = this.start + this.visibleCount
this.transform = `translate3d(${this.itemWidth * this.start}px,0,0)`
}
复制代码
start
的计算通过scrollLeft
除每一列的宽度并向下取整,end
则是可视区域的width
(clientWidth
) 除 每一列的宽度 并向上取整,知道了start
与end
两个指针之后,就可以对表头列数据columns
进行截取了。
在vue中使用computed去计算可视区域的元素值:
computed: {
visibleColumns({ columns }) {
return columns.slice(
Math.max(this.start, 0),
Math.min(this.end, columns.length)
)
}
},
复制代码
说明: 每一列宽度不一定是等宽的,这时候可以通过维护一个递减的值去获得最终start跟end的指针坐标(这里主要体会虚拟滚动的原理)如:
let startX = scrollLeft
let endX = scrollLeft + this.$el.clientWidth
const start = columns.findIndex((col) => {
const colW = col.width || 100
startX = startX - colW
return startX < colW
})
const end = columns.findIndex((col) => {
const colW = col.width || 100
endX = endX - colW
return endX < colW
})
复制代码
虚拟滚动如何优化
- 节流:遇到
scroll
滚动的场景,节流能让事件的触发根据delay的事件而变得井然有序。 requestAnimationFrame
:scroll回调的处理可以使用requestAnimationFrame
能最大程度保证画面不掉帧,因为setTimeout无法保证执行时机,而requestAnimationFrame
在没帧渲染完后都会去执行其中的回调。
transform
:对于窗口的位移,大多数人都是使用padding-left
处理,但这会不断产生重绘与回流,根据render tree渲染成图层到合成图层的过程,一旦同一个图层其中元素产生变更,就会重新去处理图层。因此可使用transform
并指定csswill-change: transform;
来告诉GUI进程这个用单独的图层去渲染。- vue”就地更新”策略–用key属性渲染等……
拓展
table组件往往不单止显示数据那么简单,如果要求在表格进行搜索、排序、选择等功能时,就可以通过slot作用域具名插槽+动态组件来对table进行拓展,这样可以达到在dataSource数据整理时,可自定义每一列可选的功能。如
源码:
最后
虚拟列表在数据量级较大,dom节点渲染很多的情况下,不失为一种几乎完美的解决方案,用上之后,打开Chrome性能页面进行测试,会发现Rendering的时间大大大的减少,因此小伙伴们快用起来吧~~
以上为个人学习见解,一隅之见,欢迎交流学习!