30行代码实现通用无限列表函数

news/2024/11/23 2:50:17/

前言: 前两天接到了一个需求,主要功能是实现类似于 B站 消息页面的那种效果,右侧几个 tab 都需要使用到无限加载的功能。
image.png

大家都知道,程序员是很懒的,不可能这几个页面全都写一遍重复的逻辑。所以在接到这个需求的时候,就开始思考能不能设计一个通用的函数,可以帮我快速的完成这个需求。🤔

一. 什么是无限列表

  1. 如果你之前不了解什么是无限列表,但是我敢保证你一定在不知情的情况下体验过这个。我们还拿 B站 举例子。如下图:
    image.png
    你会发现,只有当你往下滚动后,这个页面的数据才会不断被填充,如果你留心的话,你会注意到右侧的滚动条会在快接近尾部的时候,又一次滑动到上面一点距离,那是因为后面那一部分数据是之后才加进来的。
    image.png

接下来让我们模拟一下这个场景,来告诉你这样设计的好处。

  1. 当用户点击左侧《系统消息》按钮的时候,假设现在数据库有100条消息,那么我们有两个方案可以选择:

      1. 后端直接返回100条数据
      1. 后端只把最前面最新的数据返回
  2. 很显而易见,站在用户的角度来讲,其实我们最希望点进来后,首屏幕显示的是最新的那几条消息,其实之前过老的消息我不是特别关心,但是我们又不能把老信息完全不给用户看。(万一用户有一个中奖消息被遗忘到了后面,用户某天突然想起来怎么办呢?)

  3. 那么这时候我们就需要将上面两种方式结合起来,我们可以假设用户刚进来此页面,我们暂时先只返回最新的前10条数据给用户,一旦当用户滚动到最下方时,我们就判断用户是想查阅更为之前时间段的消息,那么我们再返回后 10 条给用户。如果用户再向下滚动,我们再返回10条…以此类推,直到100条数据全部返回。

  4. 体会到了上面的场景,我想你应该已经理解了无限列表存在的意义。

二. 传统无限列表

  1. 在动手处理这个需求之前,我参考了网上之前已经有的现成的轮子和大部分无限列表的设计思路,发现了很多都是通过观察 scroll 事件来判断容器是否已经滚动到底部。

    • 如果到底:发起请求,将新获得的数据 push 到数组中。
    • 没到底:不做任何处理。
  2. 我们写一个简单的 demo 来模拟一下这个场景。简单来讲就是一个容器 div 放了过多的内容,导致了溢出,并且我们设置了 overflow-auto ,使容器可以向溢出的方向滚动。
    这里我为了方便掩饰,创建了一个长度为 10 的空数组,然后通过 v-for 去渲染,每一个 div 固定一个长度 100 px。
    image.png
    渲染的结果如下:
    1.gif

  3. 根据上面我们刚刚提到的无限列表的逻辑,现在我们需要判断用户什么时候 “滚到底了”。这里第一步需要给当前的容器元素绑定 scroll 事件。
    image.png
    这样我们就可以获取到容器元素的 scrollTop 属性。

  4. 可能你还会有疑问🤔,拿到 scrollTop 有什么用呢?确实,我们单单知道这一个属性是没有什么用的,我们需要搭配使用另外两个十分重要的属性一起使用,才可以达到我们的目的。那就是 clientHeight 还有 scollHeight。这里有一个触底计算方法。

      clientHeight + scrollTop = scrollHegiht
    
  5. 最开始看到这个计算方法的时候,我也很迷惑,为什么这样就表示到底了呢?这几个属性我之前的文章里有详细解释。

    🎁你必须知道的 clientWidth, offsetWidth, scrollWidth.

    如果你懒得看,没关系,我接下来会简单描述一下,不会影响你进一步阅读本文的主主体内容。

    • clientHeight 代表我们容器内容区域的高度。更加直观来讲,当你元素溢出了,并且你设置一个 overflow-hidden,那么忽略溢出的内容,你可以直接看到的区域就是 clientHeight ,也就是这一部分的高度。
      image.png
    • scrollTop 代表我们容器向下滚动了多少高度。这里为了更好的表现出 scrollTop,我们在控制台输出一下。可以看到 scrollTop 随着我们向下滚动,值越来越大。
      2.gif
    • scrollHeight 其实代表着这个元素实际的高度,因为人家本来就这么高,只不过之前你给容器设置了 overflow-auto ,把人家的高度给隐藏了一部分,现在还给人家了而已。
      为了更直观的看到这个属性的含义,我们把容器的 overflow-auto 设置为 overflow-visible
      3.gif
      我们验证一下,我们已知道每个元素的高度是 100px,现在有 10 个元素,那么如果我们推断的没错,那么 scroll 的值应该 100px * 10 = 1000px。让我们选中这个容器高度,在选项卡中搜索 scrollHeight,可以看到我们的猜想没错,它代表的就是实际高度。
      image.png
  6. 大概了解了这三个属性的含义,那么我们回过头再来看我们的触底公式。

    clientHeight + scrollTop = scrollHegiht
    

    在这里你需要理清一个非常重要的细节,我们的 scrollTop 的值是有极限的,即使你滚动到底了,那么还是会有一个可视区域的高度在你眼前,它是不可能滚动到最后一个元素也看不见的。如下图:
    image.png
    当第10个元素出现的时候,其实你已经无法滚动了,此时的 scrollTop 就是最大值。也代表着不可见元素(被隐藏的元素)的总高度。

  7. 想清楚上面这个细节,我们就可以反推出当容器滚动到底的情况, (不可见元素高度scrollTop),加上当前可视区域的高度(clientHeight) 不正好就是实际的总高度嘛!(scrollHeight)。此时正好对上了我们的触底公式,此时也正是在底部的时候。

  8. 根据上面的触底公式,我们很容易的可以写出下面的判断逻辑。
    image.png
    让我们验证一下是否可行。
    4.gif

  9. 为了模拟更真实的情况,我们在触底的时候,改变数组的长度。
    image.png
    再来看一下效果
    5.gif
    可以看到我们的数组长度从本来的 10,变为了 20。

  10. 随着滚动,重复上述步骤,其实就是传统无限列表的实现原理。但是我们大家都知道,获取 clientHeight 等这些属性浏览器为了保证拿到最新的数据是会引起重绘的,并且 scroll 事件触发的频率极高,但是这个场景下又不能做节流和防抖。那有没有更好的解决方法呢?🤔

三. 转变思路

  1. 我们把之前 scroll 相关的函数和属性都去掉,接下来我们在容器元素内加上一个垫底元素,说白了,就是容器元素的最后一个子元素。
    image.png
    现在的样式大概是这样的:
    6.gif
    理所当然的滚动到底部,就会看到我们的垫底元素。

  2. 那我们的思路是否就可以从判断元素的触底公式转变为 => 什么时候看到垫底元素 了呢?那怎样判断才能优化浏览器的事件还能完美达成我们的无限列表加载呢?

  3. 接下来引入我们今天的主角,IntersectionObserver,你可以直接翻译中文----交叉观察者

四. IntersectionObserver API

  1. 具体的细节的介绍,你可以点击下方查看,在文中我只会介绍这个 API 的核心功能。不过我还是强烈建议你先查阅以后再开观看,能让你更深入理解本文的思想。(T.T 真不是我懒,真的是阮大讲的太好了,我就不再献丑了,我只把我的设计思想告诉大家,相信大家都是很聪明的!)

    • MDN IntersectionObserver

    • 阮一峰 intersectionObserver 教程

  2. 首先你必须知道的一点,这个 api 是一个构造函数,可以接受一个函数作为参数。所以你第一步的使用方式应该像下面这样。
    image.png

  3. 在此之前,我们先做一下准备工作。
    image.png
    你需要在真实元素挂载以后,调取 observer 实例对象身上的 observe 方法,它接收一个真实 dom 作为参数。这里我们把垫底元素放进去观察,具体怎么个观察法,我们接下来会讲到。

  4. 当你成功开始观察时,你的回调函数会被触发,可以在控制台上打印一下我们回调函数的参数,可以看到一个叫做 IntersectionObserverEntry的类型变量。为什么是数组呢?因为这个 api 允许你同时观察多个元素,所以这个参数才是数组。
    image.png

5.接下来我们重点就是要去处理回调函数里的逻辑,在这里我直接讲重点,由于我们只观察了一个元素,所以我们的回调函数的参数重,垫底元素就是 entries[0]。我们控制台打印一下这个变量。
image.png
可以看到这个变量身上有很多属性。
image.png

  1. 这里我直接讲重点,我们暂时只需要关心这个 intersectionRatio 的值。这个值代表着垫底元素视口元素的交叉比例。你可以暂时简单的理解整个文档的根元素。
    image.png

  2. 当我们页面没有发生滚动时,我们假设红色方块为我们被隐藏了的垫底元素,现在你的视野里是没有它的,所以交叉比例为0。
    image.png

  3. 当我们滚动到底部的时候,这时候的交叉比例就是 100%,也就是 1。但是在这里你需要用到这个 api 的第二个参数才能看到这个情况。
    image.png
    让我们设置一个叫做 threshold 的属性值(阈值)这样你就可以指定到达交叉的比例时再触发回调函数。
    image.png
    通过下面的 gif 图可以看到,只有当我们元素完全出现的时候,才会触发回调函数。(tips:第一次打印是因为这个 api 初始化的时候会默认执行一次。)
    7.gif

  4. 那么接下来的我相信你应该明白我的意思了,我们只需要在交叉比例为1 的时候,去发起请求即可。
    image.png
    让我们看一下效果:
    8.gif
    可以看到,我们已经完美复刻了传统的无限列表方案,并且这个 api 是异步执行的,只会在主进程闲下来的时候再执行回调函数,避免了我们手动优化 scroll 事件带来的负面影响。

五. 设计一个通用函数

  1. 我们再回过头看一下 B 站的左侧 tab,会发现这几个页面都是很类似的,所以我们可以设计一个函数来封装一个通用的 IntersectionObserver 函数。
    image.png

  2. 你可以搭配标题六来观看本小节,首先这个函数会返回一个 init 函数 和一个响应式变量 list

  3. 接下来我讲解一下我的设计思路。

  4. 首先这个函数需要接收一个函数作为参数,这个参数就是你每个页面去请求后端的那个函数。我在函数内部封装了一个叫做 fetchData 的函数,它会在某些条件下去请求后端,不断填充我们的 list 变量。
    image.png

  5. 核心函数其实就是 init,我们需要借助 vue3 组合式 api,来封装好它。注意,这个 init 需要接收一个容器元素作为参数,因为需要给这个传进来的容器元素添加垫底元素来判断是否已经滚动到底部了。
    image.png

  6. 首先第一次加载的时候,我们需要默认填充一次我们的 listimage.png

  7. 然后我们在 nextTick 里去动态添加一个垫底元素
    image.png

  8. 紧接着开启观察者 API 来判断交叉比例,如果为 1,那么调取 fetchData函数 填充我们的 list 即可。
    image.png

  9. 接下来你只需要在每个需要用到的页面里去调取这个函数即可。

六. 源码

import { ref, nextTick } from "vue";export function useInfiniteLoad(fetchListFn: () => Promise<any[]>) {const data = ref<any[]>();const list = ref<any[]>([]);async function fetchData() {data.value = await fetchListFn();list.value.push(...data.value);}// observerFnasync function init(containerEl: HTMLElement) {await fetchData();if (!containerEl) return;await nextTick(() => {const dom = document.createElement("div");dom.setAttribute("id", "loadmore");dom.style.height = "1px";dom.textContent = " ";containerEl.appendChild(dom);const observer = new IntersectionObserver((entries: IntersectionObserverEntry[]) => {const ratio = entries[0].intersectionRatio;if (ratio === 1) {fetchData();}});observer.observe(dom);});}return { init, list };
}

七. 结语

这个函数仅仅只是启发你的设计思路,并不能在实际项目中完全满足你的需求,我在项目中用到的函数其实是根据我们后端分页设计来完善的,但是总体的思想是不变的,你需要做的根据项目来封装你学到的内容。🎁


http://www.ppmy.cn/news/400052.html

相关文章

如何学习Java“高并发”,并在项目中实际应用?

前几天收到一位粉丝私信&#xff0c;说的是他才一年半经验&#xff0c;去面试却被各种问到分布式&#xff0c;高并发&#xff0c;多线程之间的问题。基础层面上的是可以答上来&#xff0c;但是面试官深问的话就不会了&#xff01;被问得都怀疑现在Java招聘初级岗位到底招的是初…

movetoThread应用的注意点

分析 官网的说明&#xff1a; void QObject::moveToThread(QThread *targetThread) Changes the thread affinity for this object and its children. The object cannot be moved if it has a parent. Event processing will continue in the targetThread. To move an objec…

【北邮国院大三下】Intellectual Property Law 知识产权基础 Week4

北邮国院大三电商在读&#xff0c;随课程进行整理知识点。仅整理PPT和相关法条中相对重要的知识点&#xff0c;个人认为相对不重要的细小的知识点不列在其中。如有错误请指出。转载请注明出处&#xff0c;祝您学习愉快。 如需要pdf格式的文件请私信联系或微信联系 PRC是否inf…

专转本-常用电脑设备

显示设备 显卡 显卡主体 1.HDMI 2.VGA 3.DVI GPU 1.生产厂家 2.电压要求 3.对应型号 CRT

九联UNT401H主板2+8桌面蓝牙语音通刷固件

九联UNT401H主板28桌面蓝牙语音通刷固件 九联UNT401H-零配置版机顶盒开机后按遥控器设置键进入系统设置界面&#xff0c;遥控器均匀按下数字键782782323即可开启adb功能&#xff0c; 机顶盒刷机前在电脑上使用一键替换rec工具替换掉原机recovery文件&#xff0c; 然后再将下载…

YOGA27多维一体电脑,兼具出色外观与高端配置

在当今科技数码行业,用户更加关注产品的外观设计。各品牌厂商在提升配置的同时,也在外观设计上下足了功夫,比如最近的联想YOGA 27多维一体电脑就交上了一份令用户满意的答卷。为什么YOGA27能够吸引消费者关注?下面将一一揭晓。 首先,联想YOGA 27的双轨设计可随意变换产品形态,…

【Spring学习之生命周期】什么是生命周期?什么是作用域?了解六种作用域

前言&#xff1a; &#x1f49e;&#x1f49e;从前⾯的课程我们可以看出 Spring 是⽤来读取和存储 Bean&#xff0c;因此在 Spring 中 Bean 是最核⼼的操作资源&#xff0c;所以接下来我们深⼊学习⼀下 Bean 对象。 前路漫漫&#xff0c;希望大家坚持下去&#xff0c;不忘初心&…

联想r720自带杜比驱动下载_暑假追剧补习神器,联想M10 PLUS评测

迈入八月&#xff0c;暑假正当时。空调wifi西瓜&#xff0c;追剧游戏宅家&#xff0c;这样的日子的确很舒服&#xff0c;“假期不努力&#xff0c;开学徒伤悲”&#xff0c;姑且不说要报各种兴趣班&#xff0c;先把主科学好就刻不容缓。联想Tab M10 PLUS专门为小学到高中的同学…