虚拟列表

cases:

  1. 定高、不定高。
  2. 横向、纵向

定高

首先有几个概念的定义如下:

  1. 可视区域。基本上指的是屏幕的可见范围浏览器的视口大小。通常高度值为 screenHeight. 可视项数 showSize = screenHeight / itemHeight
  2. 可滚动区域/真实高度。listHeight = itemHeight * totalSize 每一项的高度,总数量。
  3. 偏移位置。一般可以直接拿 scrollTop 来说,含义就是元素的顶部具体屏幕的顶部的高度。
  4. 起始位置(下标)。 startIndex = scrollTop / itemHeight 感觉应该是向上取整???
  5. 结束位置(下标)。 endIndex = startIndex + showSize showSize 是上面计算出的可视项数
  6. 偏移量。 offSet = scrollTop - scrollHeight

简单步骤:

  1. 根据单个元素高度计算出滚动容器的可滚动高度,并撑开滚动容器;
  2. 根据可视区域计算总挂载元素数量;
  3. 根据可视区域和总挂载元素数量计算头挂载元素(初始为 0)和尾挂载元素;
  4. 当发生滚动时,根据滚动差值和滚动方向,重新计算头挂载元素和尾挂载元素。

不定高

差别:

  1. 默认先给一个高度。
  2. 全量。对每一项的节点,都执行一个当前这个 node 的 getBoundingClientRect# 方法,获取 rect.height 作为每一项的高,并缓存下来。同同时缓存下来的还有,top 距离顶部的高度,距离底部的高度 bottom, 以及下标 index。
  3. 计算属性将开始节点位置的节点的所有高度相加,得到 offsetTop 用 paddingTop 做偏移撑开容器。
  4. 滑动事件的时候,判断上滑还是下滑。 下滑的话,从缓存中找到第一个比 bottom 比 scrollTop 还要大的值。下滑的话找到第一个 top 比 scrollTop 大的值。找到之后,拿到该值的 index 与当前的 startIndex 作对比,更新它,重新取 list

详细步骤

  1. 监听父容器的 scrollTop 事件,获取滚动位置 scrollTop
  2. 可视区域高度固定 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)

1. 计算可视条目

2. 批量 DOM 操作

3. 事件

4. 缓存计算结果

可以采取一个非常简单的缓存策略,记录最后一次计算尺寸、偏移的 index 。

5. 事件控制

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

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

table + 虚拟列表

ant table 的虚拟列表.

包括 ant design vue 的 table 也好,element 的 table 也好,如果给到的数据量比较大的话, 我们可以在 Performance 中看到,Rendering 的时间会爆炸式增长,会远远比 Scripting 要高。可见 table 组件只适合渲染小数据量的,或者要进行合理的分页。不然如果渲染 1000+ 多条数据部分页的情况下, 点击一下筛选框或者多选框之类的都会很卡顿。

长列表的性能问题,主要体现在两个方面:

  1. 初次渲染耗时长
  2. 滚动时有迟滞感

原因在于渲染长列表需要相应数量的 DOM 元素,DOM 元素多,渲染时定位、重绘的工作量就大,也会暴露很多平时不被注意的性能问题。

优化思路:

  1. 表格数据使用 Object.freeze(data)处理,不让 vue 劫持。因为一般来说表格中的数据是不会进行更改的。一般进行更改后都是重新调用接口来重刷一遍数据。这样,vue 不会做 getter 和 setter 的转换,即这个数据不是响应式的了,可以提高表格渲染的性能。
  2. 合理使用计算属性,但也不是必须的。可以先将值预先转化好,而不是直接交给计算属性自己去计算。
  3. 引入 table 的虚拟列表

横向、纵向的虚拟列表:vxe-table

场景

如何渲染几万条数据并不卡住界面

在不卡住页面的情况下渲染数据,也就是说不能一次性将几万条都渲染出来,而应该一次渲染部分 DOM,那么就可以通过 requestAnimationFrame 来每 16 ms 刷新一次。

setTimeout(() => {
  // 插入十万条数据
  const total = 100000;
  // 一次插入 20 条,如果觉得性能不好就减少
  const once = 20;
  // 渲染数据总共需要几次
  const loopCount = total / once;
  let countOfRender = 0;
  let ul = document.querySelector("ul");

  function add() {
    // 优化性能,插入不会造成回流
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < once; i++) {
      const li = document.createElement("li");
      li.innerText = Math.floor(Math.random() * total);
      fragment.appendChild(li);
    }
    ul.appendChild(fragment);
    countOfRender += 1;
    loop();
  }
  function loop() {
    if (countOfRender < loopCount) {
      window.requestAnimationFrame(add);
    }
  }
  loop();
}, 0);

前端长列表的性能优化。只渲染页面用用户能看到的部分。并且在不断滚动的过程中去除不在屏幕中的元素,不再渲染,从而实现高性能的列表渲染。

插入几万个 DOM,如何实现页面不卡顿?

肯定不能一次性把几万个 DOM 全部插入,这样肯定会造成卡顿,所以解决问题的重点应该是如何分批次部分渲染 DOM。部分人应该可以想到通过 requestAnimationFrame 的方式去循环的插入 DOM,其实还有种方式去解决这个问题:虚拟滚动(virtualized scroller)。

这种技术的原理就是只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容。

滚动

从上图中我们可以发现,即使列表很长,但是渲染的 DOM 元素永远只有那么几个,当我们滚动页面的时候就会实时去更新 DOM,这个技术就能顺利解决这发问题。如果你想了解更多的内容可以了解下这个 react-virtualizedopen in new window

无限下拉的时候如何实现懒加载,并且如何保证 vuex 之类的堆栈不会爆掉(或者保持一定的速度)

100 亿排序问题:内存不足,一次只允许你装载和操作 1 亿条数据,如何对 100 亿条数据进行排序

  • 把这 100 亿的 int 型数据以文件形式存储到 100 个小文件中
  • 对这 100 个小文件分别读取后排序再存入
  • 遍历排序后对 100 个小文件,每个小文件里面取第一个数字, 组成一个 100 大数的堆
  • new 个空的大文件存最后的结果
  • 之后出 100 个数的那个堆,找到对应的小文件取数字,写入大文件, 记得 flash, gc 之类的
  • 循环 3, 等所有的小文件都取完, 大文件就是存的最后结果

文档参考

Last Updated:
Contributors: yiliang114