近期由于要实现 FunCDN 的实时访客记录,对于该类页面实现,通常直接使用
无序列表形式
<ul> <li>Some Thing</li> </ul>
表格形式
<table> <tr> <th>Field</th> </tr> <tr> <td>Some Thing</td> </tr> </table>
在少量元素下,这类实现可以很轻松的铺设页面,也可以对子元素设置更丰富的CSS样式,但是由于浏览器性能有限,铺设大量的元素会导致浏览器渲染压力倍增,内存爆表,所以需要对其进行一些优化以提升用户体验。在本次优化中,参考了 Surely 这个 UI 组件,该组件是由 AntDesign 团队所开发的高性能Table 组件库,可用于对海量数据的表格进行渲染以及操作。对于该组件进行分析然后剖析出其实现的虚拟列表原理。
首先来了解下浏览器原生方法实现的列表性能,在绝大部分情况下,列表是一个限定高度的容器(不限制高度也会被显示器限制高度,除非像 iPhone 2000 那样(狗头))元素会全部渲染并且撑开容器的滚动条,这样才可以让用户去浏览全部的数据,在初次加载和滚动过程中,浏览器会对其进行渲染。打个比方,如果一个列表由 10 万 条数据所构成,那浏览器在第一次加载时,需要渲染至少 10 万个DOM节点,如果对元素进行了更复杂的设计,那渲染 DOM数 将呈几何量级倍增,可想而知对浏览器的渲染压力有多大。所以在对海量数据进行渲染时,就需要祭出虚拟列表这个大杀器。
图1
如图1 所示,渲染1万个无序列表所耗的CPU时长和内存都是巨额的。而在我们仅仅增加了一丢丢的样式时(style=”color:red”),代价却是巨大的,如图2所示。
图2
性能对比图
接下来我们聊聊虚拟列表是如何实现的,由于 Vue 是数据驱动型前端框架,所以我们可以对数据进行改动从而使得显示效果改变。上文已经描述了页面卡顿是由于海量DOM节点导致的,所以我们可以对DOM数量进行一些限制。
我们将一个虚拟列表拆分为3个区域,分别为
可见区域:即用户实际看到和操作的区域
缓冲区域:用户在滚动时,由缓冲区域来提供视图
无DOM区域:该区域内没有任何DOM节点,仅仅是为了滚动条能呈现,而扩充 容器内部 高/宽度 的区域
在这样的布局下
- 我们将一个无序列表 ul 容器 外部再套上一层容器(class=”container”),限制展示高度,并启用纵向滚动条
- 对外部容器绑定滚动事件(@scroll=”handleScroll”)
- 对容器内实际存放DOM的 ul 标签绑定 style 属性 (listStyle),用以设置扩充高度
- li 此时渲染的列表数据则不再是原始数据,而是 renderList
- 给 li 同时也绑定上 itemStyle ,用以设置元素的transform值,即使用translateY的特性来偏移显示
- 设置 3个 常量 viewLength、bufferLength、itemHeight,分别代表的是 可见区元素数量、缓冲区元素数量、元素高度
- 声明数据:list(原始数据)、renderList(渲染数据)、listStyle(ul 标签的样式)、itemStyle(元素的样式)
- 声明方法:handleScroll 用以触发滚动事件的回调处理
- 当用户触发事件时,通过外部容器的滚动高度来计算元素在原始数据中的偏移量
- 根据元素偏移量,计算出元素的高度偏移量,并赋值给元素样式
- 重新给renderList赋上符合该滚动区域的值
在本文的最后将会送上业务实现代码,给大家做参考,先来看下性能对比图把(狗头)
总结
虽然在脚本执行的CPU时长增加了,但是这种增加并非是线性增长的,我们再补充了一个测试,和上面生成1万条数据相比,用虚拟列表生成了10万条数据进行测试,可以看到在渲染和绘制的时间消耗上并没有太大的变化,甚至用时更少了。OK,下课!
<template> <div> <button @click="generate">生成数据</button> <!-- <ul> <li v-for="(val, index) of list" :key="index"> <span style="color: red">{{ val }}</span> </li> </ul> --> <div class="container" @scroll="handleScroll"> <ul :style="listStyle"> <li v-for="(val, index) of renderList" :key="index" :style="itemStyle"> {{ val }} </li> </ul> </div> </div> </template> <style scoped lang="less"> .container { height: 600px; overflow-y: scroll; ul { li { height: 100px; text-align: center; color: red; border-bottom: solid #eee 1px; } } } </style> <script> // 可视区元素数量 const viewLength = 6 // 缓冲区元素数量 const bufferLength = 4 // 元素高度 const itemHeight = 100 export default { data () { return { list: [], renderList: [], listStyle: { height: '100px', padding: '0px' }, itemStyle: { transform: `translateY(0px)` } } }, methods: { generate () { const arr = [] for (let i = 0; i < 10000; ++i) { arr.push(i.toLocaleString()) } this.list = arr // 扩充列表高度 this.listStyle.height = `${this.list.length * itemHeight}px` // 排布可见区和缓冲区数据 this.renderList = this.list.slice(0, viewLength + bufferLength) }, handleScroll (e) { // 当前滚动高度 const scrollTop = e.target.scrollTop // 根据当前滚动高度计算元素偏移量 const itemOffset = Math.floor(scrollTop / itemHeight) // 根据元素偏移量计算元素的高度偏移量 // 例如:滚动高度为240,元素高度为100,则元素高度偏移量为 Math.floor(240/100)*100 const offsetY = itemOffset * itemHeight this.itemStyle.transform = `translateY(${offsetY}px)` // 重新排布可见区和缓冲区数据 this.renderList = this.list.slice(itemOffset, itemOffset + viewLength + bufferLength) } } } </script>