长列表网页相信大多数开发者都遇到过,在 DOM 元素过多的情况下,浏览器渲染会很慢,非常影响用户体验;
因此我们会经常采用虚拟滚动、分页、上拉加载更多等不同的方式来进行优化,这些方式的思想都是一样的,都是只渲染可见区域,等用户需要时再加载更多的内容;
虚拟列表核心原理
-
整个虚拟列表划分为三个区域,分别是上缓冲区(0/2个元素),可视区(n个元素),下缓冲区(2个元素);
-
当滚动到一个元素离开可视区范围内时,就去掉上缓冲区顶上的一个元素,然后再下缓冲区增加一个元素,这就是虚拟列表的核心原理了;
虚拟列表的实现
元素固定高度
-
实现:
- 首先先计算出由 1000 个元素撑起的盒子(称之为 container )的高度,撑开盒子,让用户能进行滚动操作;
- 计算出可视区的起始索引、上缓冲区的起始索引以及下缓冲区的结束索引(就像上图滚动后,上缓冲区的起始索引为 2,可视区起始索引为 4,下缓冲区结束索引为 9);
- 采用绝对定位,计算上缓冲区到下缓冲区之间的每一个元素在 contianer 中的 top 值,只有知道 top 值才能让元素出现在可视区内;
- 将上缓冲区到下缓冲区的元素塞到 container 中;
-
实现代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> #container { width: 200px; height: 200px; position: relative; overflow: auto; background: #eee; } .virtually-item:nth-child(odd) { position: absolute; width: 100%; text-align: center; background-color: bisque; } .virtually-item:nth-child(even) { position: absolute; width: 100%; text-align: center; background-color: aquamarine; } </style> </head> <body> <!-- 容器 --> <div id="container"></div> <script> const domContainer = document.getElementById('container'); const FixedSizeList = ({ height, itemSize, itemCount }) => { // 记录滚动掉的高度 const scrollOffset = domContainer.scrollTop; const getCurrentChildren = () => { // 可视区起始索引 const startIndex = Math.floor(scrollOffset / itemSize); // 上缓冲区起始索引 const finialStartIndex = Math.max(0, startIndex - 2); // 可视区能展示的元素的最大个数 const numVisible = Math.ceil(height / itemSize); // 下缓冲区结束索引 const endIndex = Math.min(itemCount - 1, startIndex + numVisible + 2); const items = []; // 根据上面计算的索引值,不断添加元素给 container for (let i = finialStartIndex; i <= endIndex; i++) { // 计算每个元素在 container 中的 top 值 const itemStyle = `height: ${itemSize}px; top: ${itemSize * i}px; line-height: ${itemSize}px;`; items.push(`<div class="virtually-item" data-inx="${i}" style="${itemStyle}">${i}</div>`); } return items; } // 1000 个元素撑起盒子的实际高度 return `<div style='height: ${itemSize * itemCount}px;'>${getCurrentChildren().join('')}</div>`; }; // 当触发滚动就重新计算 domContainer.addEventListener('scroll', function (event) { domContainer.innerHTML = FixedSizeList({ height: 200, itemSize: 50, itemCount: 1000 }); }); domContainer.innerHTML = FixedSizeList({ height: 200, itemSize: 50, itemCount: 1000 }); </script> </body> </html>
元素不定高度
-
难点一:由于每个元素高度不一,无法直接计算出 container 的总高度;
- 可以通过遍历所有的 Row 计算出总高度,但计算出精确总高度的必要性不大,同时也为了兼容第三种虚拟列表,不去计算精确的总高度;
- 现在回到出发点,思考 container 的高度的作用是什么?其实就是为了足够大,让用户能进行滚动操作,那可以自己假设每一个元素的高度,在乘上个数,弄出一个假的但足够高的container 让用户去触发滚动事件,当然这种方案会带来一些小 bug(这个 bug 的影响不大,可以忽略);
-
难点二:每个元素高度不一,每个元素的 top 值不能通过 itemSize * index 直接计算出 top 值;
-
难点三:每个元素高度不一,不能直接通过 scrollOffset/itemSize 计算出已被滚动掉的元素的个数,很难获取到可视区的起始索引;
- 其实难点二和难点三本质都一样,元素高度不一,导致不知道被滚动掉了多少元素,只要知道被滚动掉的元素的个数,top 值和索引都迎刃而解;
- 可以采用这种解决方案,那就是每次只需要计算上缓冲区到下缓冲区之间的元素,并记录他们,并且记录下最底下的那个元素的索引,当用户进行向上滚动时,就可以直接从已经计算好的记录里取,如果向下滚动,根据上一次记录的最大的索引的那个元素不断累加新元素的高度,直到它大于已经滚动掉的高度,此时的索引值就是可视区的起始索引了,这个起始索引所对应的 top 就是累加的高度;
- 图解:每一个元素的 top 值都能通过上一个元素的 top 值 + 上一个元素的 height 计算出来;
- 举个例子,假设我们需要知道 item14 的 top 值;
- 先在记录里找有没有 item13 的数据,如果有则就拿 item13.top + item13.heighht 得到 item14 的 top;
- 如果记录中没有(由上图得知只记录了item1-item10 的数据),则拿到记录中最后一个元素的数据(item10)进行累加,先计算并记录 item11 的,再计算并记录 item12 的,再计算并记录item13 的,最后就是 item14 的了;
元素动态高度
最后这一种虚拟列表其实就是基于第二种来实现的,只不过增加监听元素高度变化事件,在某个元素发生变化的时候重新计算各种数据;
主题切换
上一篇