Vue 虚拟列表的实现与原理(解决海量dom导致的页面卡顿)

近期由于要实现 FunCDN 的实时访客记录,对于该类页面实现,通常直接使用 无序列表形式 <ul&gt…

近期由于要实现 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节点,仅仅是为了滚动条能呈现,而扩充 容器内部 高/宽度 的区域

在这样的布局下

  1. 我们将一个无序列表 ul 容器 外部再套上一层容器(class=”container”),限制展示高度,并启用纵向滚动条
  2. 对外部容器绑定滚动事件(@scroll=”handleScroll”)
  3. 对容器内实际存放DOM的 ul 标签绑定 style 属性 (listStyle),用以设置扩充高度
  4. li 此时渲染的列表数据则不再是原始数据,而是 renderList
  5. 给 li 同时也绑定上 itemStyle ,用以设置元素的transform值,即使用translateY的特性来偏移显示
  6. 设置 3个 常量 viewLength、bufferLength、itemHeight,分别代表的是 可见区元素数量、缓冲区元素数量、元素高度
  7. 声明数据:list(原始数据)、renderList(渲染数据)、listStyle(ul 标签的样式)、itemStyle(元素的样式)
  8. 声明方法:handleScroll 用以触发滚动事件的回调处理
    1. 当用户触发事件时,通过外部容器的滚动高度来计算元素在原始数据中的偏移量
    2. 根据元素偏移量,计算出元素的高度偏移量,并赋值给元素样式
    3. 重新给renderList赋上符合该滚动区域的值

在本文的最后将会送上业务实现代码,给大家做参考,先来看下性能对比图把(狗头)

性能监测图

 

总结

虽然在脚本执行的CPU时长增加了,但是这种增加并非是线性增长的,我们再补充了一个测试,和上面生成1万条数据相比,用虚拟列表生成了10万条数据进行测试,可以看到在渲染和绘制的时间消耗上并没有太大的变化,甚至用时更少了。OK,下课!

性能监测图(10万条数据)

最终性能对比图

<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>
本文来自网络,不代表随客网立场,转载请注明出处。
下一篇
虚拟列表和原生列表性能对比

已经没有了