🦊【前端面试】列表如何做,看这一篇就够啦——触底加载、虚拟滚动与计算展现值🦄️

1,427 阅读4分钟

我们作为前端,平时遇到的一大类工作就是展现各种的数据,而列表就是其中一个基础而通用的形式,无论是像Google、百度这样的搜索引擎,到像Facebook、Twitter、掘金这样的用户社区,抑或是像美团、饿了么这样的本地服务,列表都是最为重要的展现形式之一。

触底加载

列表加载数据的交互形式主要有分页和触底加载两种形式。由于触底下载更适应于移动设备,同时对于类似推荐这种列表中的项目用个数不确定的情景更适应,因此触底加载成为了目前相对主流的选择。

触底加载的核心在于判断何时满足触底的条件。最直观的一种方法,就是在滚动的过程中,判断滚动元素的scrollTop+clientHeight是否已经接近或超过scrollHeight。回顾一下这几个属性的定义: image.png

由于scroll事件触发非常频繁,为了提升性能,我们需要对回调函数进行throttle,从而降低执行的频度。同时,在调用后端接口等待结果的过程中,哪怕滚动位置再次满足了条件,也不需要再次发起接口请求。

我们提交了这份代码,看到如丝般顺滑的加载,内心很满意。直至隔壁座的后端小哥跑过来说,我把窗口最大化为啥底下没内容呀,我看接口没报错呀。

嗯~原来不只是滚动的时候,元素大小变化也需要监控到!那么该怎么做呢?浏览器提供了ResizeObserver接口,我们可以创建一个ResizeObserver并用它来监控我们的滚动元素(或者大多数情况下直接监控body元素),在元素尺寸变更时,依然根据scrollTopclientHeightscrollHeight的关系判断是否需要进行加载。

const resizeObserver = new ResizeObserver(entries => {
  if(entries[0].scrollTop + entries[0].clientHeight >= entries[0].scrollHeight) {
    // 加载更多
    throttledLoadMore();
  }
});
resizeObserver.observe(
  document.querySelector('.scroll')
);

除了判断滚动位置,触底的判断其实还有另一种方式。我们可以在列表的底部放置一个透明的footer,同时利用IntersectionObserver判断这个footer是否出现在可视区域内。这种方式就不用额外处理元素大小变化的问题了。

const intersectionObserver = new IntersectionObserver(entries => {
  // 如果footer不可见,就返回
  if (entries[0].intersectionRatio <= 0) {
    return;
  }
  // 加载更多
  throttledLoadMore();
});
intersectionObserver.observe(
  document.querySelector('.scrollerFooter')
);

相比于分页的形式,触底加载对数据一致性的要求更高,因为不同页的数据会被展现在同一个列表上,如果在不同页的请求的间隔内,有数据发生了变化,那么有可能导致不同页的数据一致性受到破坏,往往就会导致请求间有数据是重复的。这时候,前端就需要根据数据的id进行去重操作,避免在列表中展现重复的元素。

虚拟滚动

触底加载已经实现啦,美滋滋的向下划划划、划划划,咦,怎么感觉越划越有些卡卡的?

打开控制台一看,好家伙,这个页面密密麻麻的都是dom节点

我们的每一个item基本都长这样⬇️

image.png

每一个item都有头像、用户名、简介、以及各种各样的信息,每条信息前还有icon,简单的计算一下一个item有20个dom节点

只要划到100个左右的item,页面光列表就已经有2000个节点,如果还有些动画什么的……性能肯定会出大问题!

实际上我们的屏幕就那么大,每次是不是只要渲染能看到的节点和上下一定的滑动缓冲区就可以了呢?每次滚动的时候,我们只需要重新计算下当前所在位置(依然是通过scrollTop来计算)和滑动缓冲区内应该展示的元素,而上下的其余部分则直接分别用一个长的空白元素进行填充,这样,哪怕我们一直向下滑,渲染的元素数量也一直都不会发生变化。 整体渲染出的DOM结构就会像下面这样:

<div style="height: 2000px;"></div>
<div style="height: 50px;"><!-- content item 41 --></div>
<div style="height: 50px;"><!-- content item 42 --></div>
<div style="height: 50px;"><!-- content item 43 --></div>
<div style="height: 50px;"><!-- content item 44 --></div>
<div style="height: 50px;"><!-- content item 45 --></div>
<div style="height: 50px;"><!-- content item 46 --></div>
<div style="height: 50px;"><!-- content item 47 --></div>
<div style="height: 50px;"><!-- content item 48 --></div>
<div style="height: 50px;"><!-- content item 49 --></div>
<div style="height: 50px;"><!-- content item 50 --></div>
<div style="height: 2000px;"></div>

另外,我们也可以用将列表以绝对定位的方式渲染的方法来实现虚拟滚动。我们只需要将外层的滚动容器设为一个相对定位的元素,并且根据元素的总量给定容器的高度,然后根据当前所在位置和滑动缓冲区内应该展示的元素,通过top对它们进行定位。react-window就是采用了这种实现方案。与上面采用长空白元素方案结果相同的DOM结构就会像下面这样:

<div style="height: 4500px; position: relative; overflow-y: auto;">
    <div style="height: 50px; top: 2000px;"><!-- content item 41 --></div>
    <div style="height: 50px; top: 2050px;"><!-- content item 42 --></div>
    <div style="height: 50px; top: 2100px;"><!-- content item 43 --></div>
    <div style="height: 50px; top: 2150px;"><!-- content item 44 --></div>
    <div style="height: 50px; top: 2200px;"><!-- content item 45 --></div>
    <div style="height: 50px; top: 2250px;"><!-- content item 46 --></div>
    <div style="height: 50px; top: 2300px;"><!-- content item 47 --></div>
    <div style="height: 50px; top: 2350px;"><!-- content item 48 --></div>
    <div style="height: 50px; top: 2400px;"><!-- content item 49 --></div>
    <div style="height: 50px; top: 2450px;"><!-- content item 50 --></div>
</div>

展现值

我们的实现的列表得到了大家的好评,在进行项目复盘的时候,产品小姐姐兴冲冲地说,既然你的列表可以根据位置计算出该渲染哪些元素,那么是不是可以算一下页面上展现了哪些元素呢?

当然是可以的!整体的逻辑和虚拟滚动非常类似,都是计算应该在界面上展现的元素。但需要注意,同一个元素在同一个列表中,只应该计算一次展现。因此对于所有我们统计过展现的元素,需要缓存下它们的id,从而避免重复计算;而在刷新列表的时候,则需要将id缓存清空,来再次将其统计进来。最后,每当有新元素展现的时候,我们就调用后端接口提交(当然这里也可以进行下throttle),之后的计算就交给我们的后端小哥吧!

更多前端面试相关🐶:

面试官发出灵魂之问-富文本如何搜索高亮?

2022最新前端面试题!已斩获字节、拼多多等大厂offer,年后还不面起来吗(上)

2022最新前端面试题!已斩获字节、拼多多等大厂offer,年后还不面起来吗(下)