虚拟列表
cases:
- 定高、不定高。
- 横向、纵向
定高
首先有几个概念的定义如下:
- 可视区域。基本上指的是屏幕的可见范围浏览器的视口大小。通常高度值为 screenHeight.
可视项数 showSize = screenHeight / itemHeight - 可滚动区域/真实高度。
listHeight = itemHeight * totalSize每一项的高度,总数量。 - 偏移位置。一般可以直接拿 scrollTop 来说,含义就是元素的顶部具体屏幕的顶部的高度。
- 起始位置(下标)。
startIndex = scrollTop / itemHeight感觉应该是向上取整??? - 结束位置(下标)。
endIndex = startIndex + showSizeshowSize 是上面计算出的可视项数 - 偏移量。
offSet = scrollTop - scrollHeight
简单步骤:
- 根据单个元素高度计算出滚动容器的可滚动高度,并撑开滚动容器;
- 根据可视区域计算总挂载元素数量;
- 根据可视区域和总挂载元素数量计算头挂载元素(初始为 0)和尾挂载元素;
- 当发生滚动时,根据滚动差值和滚动方向,重新计算头挂载元素和尾挂载元素。
不定高
差别:
- 默认先给一个高度。
- 全量。对每一项的节点,都执行一个当前这个 node 的
getBoundingClientRect#方法,获取 rect.height 作为每一项的高,并缓存下来。同同时缓存下来的还有,top 距离顶部的高度,距离底部的高度 bottom, 以及下标 index。 - 计算属性将开始节点位置的节点的所有高度相加,得到 offsetTop 用 paddingTop 做偏移撑开容器。
- 滑动事件的时候,判断上滑还是下滑。 下滑的话,从缓存中找到第一个比 bottom 比 scrollTop 还要大的值。下滑的话找到第一个 top 比 scrollTop 大的值。找到之后,拿到该值的 index 与当前的 startIndex 作对比,更新它,重新取 list
详细步骤
- 监听父容器的 scrollTop 事件,获取滚动位置 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)
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+ 多条数据部分页的情况下, 点击一下筛选框或者多选框之类的都会很卡顿。
长列表的性能问题,主要体现在两个方面:
- 初次渲染耗时长
- 滚动时有迟滞感
原因在于渲染长列表需要相应数量的 DOM 元素,DOM 元素多,渲染时定位、重绘的工作量就大,也会暴露很多平时不被注意的性能问题。
优化思路:
- 表格数据使用 Object.freeze(data)处理,不让 vue 劫持。因为一般来说表格中的数据是不会进行更改的。一般进行更改后都是重新调用接口来重刷一遍数据。这样,vue 不会做 getter 和 setter 的转换,即这个数据不是响应式的了,可以提高表格渲染的性能。
- 合理使用计算属性,但也不是必须的。可以先将值预先转化好,而不是直接交给计算属性自己去计算。
- 引入 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-virtualized。
无限下拉的时候如何实现懒加载,并且如何保证 vuex 之类的堆栈不会爆掉(或者保持一定的速度)
100 亿排序问题:内存不足,一次只允许你装载和操作 1 亿条数据,如何对 100 亿条数据进行排序
- 把这 100 亿的 int 型数据以文件形式存储到 100 个小文件中
- 对这 100 个小文件分别读取后排序再存入
- 遍历排序后对 100 个小文件,每个小文件里面取第一个数字, 组成一个 100 大数的堆
- new 个空的大文件存最后的结果
- 之后出 100 个数的那个堆,找到对应的小文件取数字,写入大文件, 记得 flash, gc 之类的
- 循环 3, 等所有的小文件都取完, 大文件就是存的最后结果