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

news2024/12/24 8:58:47

前言: 前两天接到了一个需求,主要功能是实现类似于 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);
 }

 // observerFn
 async 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.coloradmin.cn/o/649299.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HBuilder连接手机模拟器调试,进行抓包

hbuilder连接手机模拟器 1.adb是什么&#xff1f;引用 2. 进行adb路径设置&#xff0c;选中模拟器中的adb路径&#xff0c;配置好端口&#xff0c;夜神模拟器端口&#xff1a;62001 3.运行到Andriod基座 在HBuilder连接模拟器过程中&#xff0c;对接口进行抓包 1.安装抓包工…

Spring Security --- 自定义Filter

简介任何 Spring Web 应用本质上只是一个 servletSecurity Filter在 HTTP 请求到达你的 Controller 之前过滤每一个传入的 HTTP请求Filter请求过滤器可以帮助进行HttpServletRequest请求和HttpServletResponse响应的过滤在自定义的Filter过滤器中可以对请求进行过滤&#xff0c…

控制并发流程,做好线程间的协调

一、概述 1. 什么是控制并发流程&#xff1f; 线程一般是由线程调度器自动控制的&#xff0c;但有些场景需要按照我们程序员的意愿去实现多线程之间相互配合&#xff0c;从而满足业务逻辑。比如&#xff1a; 让线程A等待线程B执行完后再执行等一些相互合作的逻辑&#xff1b…

【表面缺陷检测】基于yolov5的钢轨表面缺陷检测(附代码和数据集,Windows系统)

写在前面: 首先感谢兄弟们的订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。 路虽远,行则将至;事虽难,做则必成。只要有愚公移山的志气、滴水穿石的毅力,脚踏实地,埋头苦干,积跬步以至千里,就…

python打包后报错,无法启动,电脑缺少api-ms-win-core-path-11-1-0.dll

参考&#xff1a;《运行打包python程序时报&#xff1a;无法启动此程序&#xff0c;因为计算机中丢失 api-ms-win-core-path-l1-1-0.dll 尝试重新安装该程序以解决此问题。》 原因&#xff1a;python版本较高&#xff0c;打包时的python版本是python3.10&#xff0c;而运行打包…

mdBook介绍及使用——使用 Markdown 创建你自己的博客和电子书

目录 介绍一、下载与创建项目1.下载2.初始化3.结构说明 二、编写文章与启动1.编写文章2.构建3.启动 mdbook 服务 三、其他配置 介绍 mdBook 是一个使用 Markdown 创建书籍的命令行工具。它非常适合创建产品或 API 文档、教程、课程材料或任何需要清晰、易于导航和可定制的演示…

LED开关电源里的PCB回路设计应该怎么做?

LED开关电源的研发速度在最近几年中有了明显的技术飞跃&#xff0c;新产品更新换代的速度也加快了许多。作为最后一个设计环节&#xff0c;PCB的设计也显得尤为重要&#xff0c;因为一旦在这一环节出现问题&#xff0c;那么很可能会对整个的LED开关电源系统产生较多的电磁干扰&…

界面控件DevExtreme UI组件——增强的自定义功能

在本文中&#xff0c;我们将回顾DevExtreme UI组件在v22.2版本主要更新中一系列与自定义相关的增强。 DevExtreme拥有高性能的HTML5 / JavaScript小部件集合&#xff0c;使您可以利用现代Web开发堆栈&#xff08;包括React&#xff0c;Angular&#xff0c;ASP.NET Core&#x…

6、微服务组件openfeign

1、在消费端的项目中引入openfeign依赖 首先需要确保引入了springcloud&#xff0c;因为openfeign依赖与springcloud 在消费端的pom.xml中引入openfeign&#xff0c;父项目中已经引入了springcloud了 <?xml version"1.0" encoding"UTF-8"?> <…

行云创新CloudOS助力蜂巢能源获中国信通院2023云原生应用优秀案例奖

2023 年 6 月 6 日&#xff0c;工业和信息化部主办的ICT 中国高层论坛-云原生产业高峰论坛成功举办&#xff0c;活动期间&#xff0c;中国信通院发布了“2023云原生应用优秀案例奖”获奖名单。其中&#xff0c;蜂巢能源作为中国新能源行业的代表之一&#xff0c;凭借其基于行云…

IKEA EDI项目开源介绍

近期为了帮助广大用户更好地使用 EDI 系统&#xff0c;我们根据以往的项目实施经验&#xff0c;将成熟的 EDI 项目进行开源。用户安装好知行之桥EDI系统之后&#xff0c;只需要下载我们整理好的示例代码&#xff0c;并放置在知行之桥指定的工作区中&#xff0c;即可开始使用。 …

一次 Nacos 导致的 CPU 飙高问题完整复盘

今天下午突然 出现 测试环境 cpu飙高&#xff0c;干到了 60%&#xff0c;其他项目 响应时间明显变长。。。有点吓人&#xff0c;不想背锅 项目背景 出问题的项目是 需要连接各个不同nacos 和不同的 namespace 进行对应操作的 一个项目&#xff0c;对nacos的操作都是httpClien…

【工具】Maven加强版 — mvnd的使用

【工具】Maven加强版 — mvnd的使用 下载 Releases apache/maven-mvnd (github.com) 选对应的版本 我用的Windows版 1、安装 直接解压。 然后配置环境变量&#xff1a;将 bin 目录添加到 PATH 2、测试 打开CMD终端&#xff0c;输入 mvnd -v 可以看到如下信息表示安装成…

Linux入侵检测学习笔记2

查看异常流量&#xff1a; iftop&#xff1a;动态显示网络接口流量信息&#xff1a; iftop工具是一款实时流量监控工具&#xff0c;可用于监控TCP/IP连接等&#xff0c;必须以root用户的身份运行。 安装方法&#xff1a; yum install -y epel-release yum install -y iftop…

云服务器docker方式部署JAVA微服务

党建后端java微服务部署步骤&#xff08;采用docker部署&#xff09; 开通dua 开通端口号&#xff1a;8848、6379、8000 - 8010、9848、9849 step1&#xff1a;安装必要的一些系统工具 sudo apt-get update sudo apt-get install ca-certificates curl gnupg step2&#xff1a…

Spark SQL典型案例

文章目录 一、实现任务1、准备数据文件2、创建Maven项目3、修改源程序目录4、添加依赖和设置源程序目录5、创建日志属性文件6、创建HDFS配置文件7、创建词频统计单例对象 一、实现任务 1、准备数据文件 在/home目录创建words.txt hello scala world hello spark world scala…

使用 docker 搭建 mongodb 6 单节点副本集

1、拉取 mongodb 镜像 docker pull mongo 2、启动一个 mongodb 的容器&#xff0c;通过副本集的形式运行 docker run --name mongoRs -d -p 27017:27017 mongo --replSet rs0 --name 创建容器的名称。 自定义 -d 以守护进程方式启动容器 -p 2701:27017&#xff1a;MongoD…

Dockerfile 使用介绍

我们使用 Dockerfile 定义镜像&#xff0c;依赖镜像来运行容器&#xff0c;因此 Dockerfile 是镜像和容器的关键&#xff0c;Dockerfile 可以非常容易的定义镜像内容&#xff0c;同时在我们后期的微服务实践中&#xff0c;Dockerfile 也是重点关注的内容&#xff0c;今天我们就…

Android系统的问题分析笔记(9) - Android 中的 Uri 如何使用呢 ?

问题 Android 中常用的 uri 如何使用呢 &#xff1f;&#xff08;此篇分析基础为Android 7.1.1系统源码&#xff09;&#xff0c;参看Android官方说明&#xff1a;https://developer.android.com/reference/android/net/Uri&#xff0c;代码可在此查看&#xff1a;https://git…

python 模块, 包

C# 中模块&#xff0c;就好像要using dll文件 python 中模块 就是python文件 包括类、方法、变量等 from 模块名 import 功能名 功能名() import 模块名 和 from 模块名 import * 模块名都引入了&#xff0c;但使用有所区别 import 模块名 使用 模块名.功能名 from 模块名 impo…