IntersectionObserver:实现滚动动画、懒加载、虚拟列表

认识

  • 浏览器自带的适用于 「监听元素与视窗交叉状态」 的观察器:IntersectionObserver(交叉观察器)
  • IntersectionObserver 是一种 JavaScript API,它提供了一种异步监测元素与其祖先容器或视口之间交叉状态的方法。简单来说,它可以告诉我们一个元素是否进入了视口或者与其祖先容器发生了交叉

  • 通过 IntersectionObserver,我们可以轻松地监听目标元素的可见性变化,进而根据这些变化来实现各种交互效果,比如懒加载图片、实现无限滚动等功能。相较于传统的事件监听方式,IntersectionObserver 更高效、灵活,可以提供更好的用户体验和性能优化。

  • 当我们创建一个 IntersectionObserver 实例时,可以指定一个回调函数,该函数在目标元素进入或离开视口时被触发。回调函数提供了一个入参IntersectionObserverEntry,其中包含了与目标元素相关的信息,例如交叉比例、目标元素的位置和大小等。

  • IntersectionObserver 还支持设定阈值,即交叉比例的百分比,用于触发回调函数。默认情况下,当目标元素至少有 0% 进入视口时,回调函数会被触发。我们可以通过设置不同的阈值来满足不同的需求

创建

IntersectionObserver API 提供了一种创建IntersectionObserver 对象的方法,对象用于监测目标元素与视窗(viewport)的交叉状态,并在交叉状态变化时执行回调函数,回调函数可以接收到元素与视窗交叉的具体数据。

  • 一个 IntersectionObserver 对象可以监听多个目标元素,并通过队列维护回调的执行顺序。
  • IntersectionObserver 特别适用于:滚动动画、懒加载、虚拟列表等场景
  • 回调异步执行,不阻塞主线程。且监听不随着目标元素的滚动而触发,性能消耗极低

API

构造函数

IntersectionObserver 构造函数 接收两个参数:

  1. 「callback」:当元素可见比例达到指定阈值后触发的回调函数

  2. 「options」:配置对象(可选,不传时会使用默认配置)

IntersectionObserver 构造函数 返回观察器实例,实例携带四个方法:

  1. 「observe」:开始监听目标元素

  2. 「unobserve」:停止监听目标元素

  3. 「disconnect」:关闭观察器

  4. 「takeRecords」:返回所有观察目标的 IntersectionObserverEntry 对象数组

// 调用构造函数 生成IntersectionObserver观察器
const myObserver = new IntersectionObserver(callback, options);

// 开始监听 指定元素
myObserver.observe(element);

// 停止对目标的监听
myObserver.unobserve(element);

// 关闭观察器
myObserver.disconnect();
构造参数
  • callback
    回调函数,当交叉状态发生变化时(可见比例超过或者低于指定阈值)会进行调用,同时传入两个参数:
    1. 「entries」IntersectionObserverEntry 数组,每项都描述了目标元素与 root 的交叉状态
    2. 「observer」:被调用的 IntersectionObserver 实例
  • option
    配置参数,通过修改配置参数,可以改变进行监听的视窗,可以缩小或扩大交叉的判定范围,或者调整触发回调的阈值(交叉比例)。
属性说明
root所监听对象的具体祖先元素,默认使用顶级文档的视窗(一般为html)。
rootMargin计算交叉时添加到根(root)边界盒bounding box的矩形偏移量, 可以有效的缩小或扩大根的判定范围从而满足计算需要。所有的偏移量均可用像素(px)或百分比(%)来表达, 默认值为"0px 0px 0px 0px"。
threshold一个包含阈值的列表, 按升序排列, 列表中的每个阈值都是监听对象的交叉区域与边界区域的比率。当监听对象的任何阈值被越过时,都会触发callback。默认值为0。

 

  •  IntersectionObserverEntry
属性说明
boundingClientRect返回包含目标元素的边界信息,返回结果与element.getBoundingClientRect() 相同
intersectionRatio返回目标元素出现在可视区的比例
intersectionRect用来描述root和目标元素的相交区域
「isIntersecting」返回一个布尔值,下列两种操作均会触发callback:1. 如果目标元素出现在root可视区,返回true。2. 如果从root可视区消失,返回false
rootBounds用来描述交叉区域观察者(intersection observer)中的根.
target目标元素:与根出现相交区域改变的元素 (Element)
time返回一个记录从 IntersectionObserver 的时间原点到交叉被触发的时间的时间戳

应用

1.懒加载

使用了 IntersectionObserver API 来监听图片元素的可见性,当图片进入视口时,将图片的 data-src 属性赋值给 src,实现图片的懒加载效果

核心是延迟加载不可视区域内的资源,在元素标签中存储srcdata-src="xxx",在元素进入视窗时进行加载。

注意设置容器的预设高度,避免页面初始化时元素进入视窗

<div class="skin_img">
  <img 
    class="lazyload" 
    data-src="//game.gtimg.cn/images/lol/act/img/skinloading/412017.jpg" 
    alt="灵魂莲华 锤石" 
  />
</div>

.skin_img {
  margin-bottom: 20px; /* 底部间距 */
  width: auto; /* 宽度自适应 */
  height: 500px; /* 固定高度 */
  overflow: hidden; /* 溢出隐藏 */
  position: relative; /* 相对定位 */
}


// 获取所有的图片节点
const imgList = [...document.querySelectorAll('img')]

// 创建一个 IntersectionObserver 实例
const observer = new IntersectionObserver((entries) => {
  entries.forEach(item => {
    // isIntersecting 是一个 Boolean 值,判断目标元素当前是否可见
    if (item.isIntersecting) {
      console.log(item.target.dataset.src) // 输出图片的 data-src
      item.target.src = item.target.dataset.src // 加载图片
      // 图片加载后即停止监听该元素
      observer.unobserve(item.target)
    }
  })
}, {
  root: document.querySelector('.root') // 指定根元素为 .root
})

// observe 遍历监听所有 img 节点
imgList.forEach(img => observer.observe(img))
2.滚动动画

用 IntersectionObserver 监听元素的可见性状态,根据元素是否进入视口来添加或移除类名,实现动画效果。CSS 代码定义了元素进入动画效果和关键帧动画,在元素进入视窗时添加动画样式,让内容出现的更加平滑

// 获取所有类名为 .observer-item 的元素
const elements = document.querySelectorAll('.observer-item')

// 创建一个 IntersectionObserver 实例,并传入回调函数 callback
const observer = new IntersectionObserver(callback);

// 遍历所有元素,为每个元素添加类名 opaque,并开始观察元素
elements.forEach(ele => {
  ele.classList.add('opaque') // 添加类名 opaque
  observer.observe(ele); // 开始观察元素
})

// 回调函数,处理 IntersectionObserver 的 entries
function callback(entries, instance) {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const element = entry.target; // 获取目标元素
            element.classList.remove("opaque"); // 移除类名 opaque
            element.classList.add("come-in"); // 添加类名 come-in
            instance.unobserve(element); // 停止观察该元素
        }
    })
}



.come-in {
  opacity: 1; /* 不透明度为1 */
  transform: translateY(150px); /* Y轴平移150px */
  animation: come-in 1s ease forwards; /* 动画效果应用于 come-in 类,持续1秒,缓动效果 */
}
.come-in:nth-child(odd) {
  animation-duration: 1s; /* 奇数序号元素动画持续时间为1秒 */
}

@keyframes come-in {
  100% {
    transform: translateY(0); /* 最终位置为Y轴向上移动0 */
  }
}

3.无限滚动

React Hooks 代码,这段代码实现了一个无限滚动加载更多数据的功能,通过 IntersectionObserver 监测最后一个元素是否进入视口来触发加载更多数据的操作,添加底部占位元素lastContentRef,在元素和视窗交叉回调时添加loading并加载新数据

// 使用 React 的 useState 钩子定义 list 和 setList,初始值为包含 10 个 null 的数组
const [list, setList] = useState(new Array(10).fill(null));
// 使用 React 的 useState 钩子定义 loading 状态,初始值为 false
const [loading, setLoading] = useState(false);

// 使用 useRef 创建一个引用对象,并初始化为 null,用于存储最后一个元素的引用
const lastContentRef = useRef(null);

// 定义 loadMore 回调函数,使用 useCallback 包裹,确保仅在依赖项改变时重新创建
const loadMore = useCallback(async () => {
    if (timer) return; // 如果 timer 存在,表示正在加载中,直接返回
    setLoading(true); // 设置 loading 为 true,表示正在加载
    await new Promise((resolve) => timer = setTimeout(() => resolve(timer = null), 1500); // 等待 1.5 秒
    setList(prev => [...prev, ...new Array(10).fill(null)]); // 将新的 10 个 null 元素添加到列表中
    setLoading(false); // 加载完成,设置 loading 为 false
}, [loading]); // 依赖 loading 状态

// 使用 useEffect 钩子监听组件挂载和更新
useEffect(() => {
    // 创建一个 IntersectionObserver 实例,观察最后一个元素是否进入视口
    const io = new IntersectionObserver((entries) => {
        if (entries[0]?.isIntersecting && !loading) { // 如果最后一个元素进入视口且未处于加载状态
            loadMore(); // 执行加载更多数据的函数
        }
    });

    // 如果最后一个元素的引用存在,则开始观察该元素
    lastContentRef?.current && io.observe(lastContentRef?.current);
}, []) // 空数组作为依赖,确保只在组件挂载时执行一次
4.虚拟列表

options参数中的rootMargin特别符合虚拟列表中缓存区的设计,我们再根据元素的可见性  element.visible ? content : (clientHeight || estimateHeight)

<template v-for="(item, idx) in listData" :key="item.id">
  <div class="content-item" :data-index="idx">
    <template v-if="item.visible">
      <!-- 模仿元素内容渲染 -->
      {{ item.value }}
    </template>
  </div>
</template>

_entries.forEach((row) => {
    const index = row.target.dataset.index; // 获取元素在列表中的索引
    // 判断是否在可视区域
    if (!row.isIntersecting) { // 如果不在可视区域
        // 离开可视区时设置实际高度进行占位 并使数据无法渲染
        if (!isInitial) { // 如果不是初始渲染
            row.target.style.height = `${row.target.clientHeight}px`; // 设置元素高度为实际高度,进行占位
            listData.value[index].visible = false; // 将列表数据中对应项的 visible 属性设为 false,使数据无法渲染
        }
    } else { // 如果在可视区域
        // 元素进入可视区,使数据可以渲染
        row.target.style.height = ''; // 清除设置的高度,使元素恢复原始状态
        listData.value[index].visible = true; // 将列表数据中对应项的 visible 属性设为 true,使数据可以渲染
    }
});

这些 DOM 是用于 「占位撑起高度」 和 「供观察器监听」,在callback时渲染成 实际内容/占位元素。

虚拟列表的核心是 「只渲染可视区内的内容」,而我们在窗口外的元素都是空div,性能开销小到忽略不计(在页面上建10w个空div都不会卡顿)。

当然这里只是简单实现,还有很多优化方向;

  1. 选取部分内容监听,避免全量监听浪费资源

  2. 合并视窗外的元素,避免空div的性能消耗和渲染成本

  3. 缓存渲染完成的DOM,避免重复渲染

  • callback 函数会在页面加载时和每次元素交叉视窗时被调用。

  • callback 函数接收两个参数:一个是 IntersectionObserverEntry 对象的数组,一个是调用该函数的 IntersectionObserver 对象。

  • IntersectionObserverEntry 对象包含了元素的交叉信息,如交叉比例(intersection ratio)和交叉区域的大小。

  • IntersectionObserver 随着页面滚动或元素变化来检测元素是否进入、退出视口。你需要考虑触发频率和处理操作的性能影响。避免在回调函数中执行过多的计算或复杂操作,以免降低页面的性能

兼容性

除了IE以外多数浏览器已经很好的支持了该功能

带来的的好处

  • 更好的性能:传统的监听滚动事件方式可能会导致频繁的计算,影响页面性能。而 IntersectionObserver 是浏览器原生提供的 API,它使用异步执行,可以更高效地监听元素是否进入视口,减少了不必要的计算和性能开销。

  • 减少代码复杂性:IntersectionObserver 可以简化代码逻辑。使用传统的方式监听滚动事件需要手动计算元素的位置、判断元素是否进入视口,以及处理滚动事件的节流等。而通过 IntersectionObserver,只需定义回调函数,在元素进入或离开视口时触发相应操作,大大简化了代码

  • 支持懒加载和无限滚动:IntersectionObserver 可以实现图片懒加载和无限滚动等常见效果。当元素进入视口时,可以延迟加载图片或触发数据请求,避免不必要的资源加载,提升页面加载速度和性能。

  • 更精确的可见性控制:IntersectionObserver 提供了更精确的可见性控制。通过设置合适的阈值(threshold),可以灵活地控制元素与视口的交叉区域达到多少时触发回调。这使得开发者可以根据需求来定义元素何时被认为是进入或离开视口,从而触发相应的操作。

总结

通过IntersectionObserver我们能够轻松获取获取元素的交叉状态,除了前文中的应用,还有诸如埋点监控、视差滚动、自动播放等多种场景都可以使用IntersectionObserver,感兴趣可以尝试。

IntersectionObserver性能表现良好,用法简洁,能够准确把控交叉的每一个阶段。它为前端带来了更好的便利性和用户体验,非常值得尝试

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/492483.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据库备份工具(实现数据定时覆盖)

数据库备份工具&#xff08;实现数据定时覆盖&#xff09; 永远热爱&#xff0c;永远执着&#xff01; 工具介绍 自动化测试数据库更新调度程序 这段 Python 脚本自动化了每天定时从生产数据库更新测试数据库的过程。它利用了 schedule 库来安排并执行每天指定时间的更新任务…

(vue)el-table表格回显返回的已勾选的数据

(vue)el-table表格编辑时回显返回的已勾选的数据 tableData数据&#xff1a; el-tableref"multipleTable":data"tableData"... >...<el-table-column prop"result" label"相关.." align"center" width"220"…

【Java程序设计】【C00344】基于Springboot的船舶维保管理系统(有论文)

基于Springboot的船舶维保管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 项目获取 &#x1f345;文末点击卡片获取源码&#x1f345; 开发环境 运行环境&#xff1a;推荐jdk1.8&#xff1b; 开发工具&#xff1a;eclipse以及i…

NX二次开发判断两对象是否相连(相交,相离)

一、概述 最近学习如何判断两根曲线是否连接&#xff08;不是相交&#xff0c;两条直线有一个端点重合&#xff09;&#xff0c;网上说到两种方法&#xff0c;一种是第一种方法&#xff0c;UF_MODL_ask_minimum_dist_3函数判断两个对象的距离&#xff0c;测得的距离等于零&…

【python】进程和线程

文章目录 进程创建进程os.fork() - 只适用于linux/unix/macmultiprocessing模块Process 类Pool进程池进程间通信队列queue常见用法管道pipes线程创建线程线程间通信互斥锁队列进程 任务管理器中一个任务就是一个进程 创建进程 os.fork() - 只适用于linux/unix/mac multipr…

Halcon与C#联合开发——1.读取图片、图像二值化

在vs中引入halcon控件 修改目标平台为 x64 拖出三个控件 代码展示 using System; using System.Windows.Forms; //引用支持halcon的命名空间 using HalconDotNet;namespace _1.HalconDisplay {public partial class Form1 : Form {// HObject 是Halcon库中表示图像和其他图形…

CentOS7下nginx部署测试

nginx部署测试 #安装程序和依赖yum install -y vim net-tools wgetyum -y install gcc pcre-devel zlib-devel openssl openssl-devel #下载nginx mkdir /opt/nginx cd /opt/nginx wget https://nginx.org/download/nginx-1.20.2.tar.gz#解压 tar zxvf nginx-1.20.2.tar.gz c…

Docker容器与虚拟化技术:OpenEuler 部署 Docker UI

目录 一、实验 1.环境 2.OpenEuler 部署 docker-compose-ui 2.OpenEuler 部署 docker ui 3.使用cpolar内网穿透 二、问题 1.docker run -w 的作用 一、实验 1.环境 &#xff08;1&#xff09;主机 表1 主机 系统架构版本IP备注LinuxopenEuler22.03 LTS SP2 192.168…

PMBOK第八版、项目管理AI标准...PMI标准今年有这些进展

项目管理实践标准不断在演变&#xff0c;PMI作为项目管理领域的权威机构&#xff0c;一直致力于与全球各行各业的项目实践者一同探索和研究最新的行业标准&#xff0c;确保PMI标准符合全球项目专业人士当前能力建设与职业发展的需要。 今年以来&#xff0c;我们发布了一系列PM…

Python提取本体文件的数据

运行结果&#xff1a; 使用replace函数去除前缀。 查找OWL的对象属性&#xff1a; 输出结果&#xff1a; 出现最后这个的原因&#xff1a; 修改程序&#xff1a; 最后的输出结果&#xff1a; 这个解析之后是这个样子的&#xff1a;

考研数学|《1800》《1000》《880》《660》最佳搭配使用方法

直接说结论&#xff1a;基础不好先做1800、强化之前660&#xff0c;强化可选880/1000题。 首先&#xff0c;传统习题册存在的一个问题是题量较大&#xff0c;但难度波动较大。《汤家凤1800》和《张宇1000》题量庞大&#xff0c;但有些题目难度不够平衡&#xff0c;有些过于简单…

代码随想录训练营第59天 | LeetCode 503.下一个更大元素II、LeetCode 42. 接雨水

目录 LeetCode 503.下一个更大元素II 文章讲解&#xff1a;代码随想录(programmercarl.com) 视频讲解&#xff1a;单调栈&#xff0c;成环了可怎么办&#xff1f;LeetCode&#xff1a;503.下一个更大元素II_哔哩哔哩_bilibili 思路 ​​​​​​LeetCode 42. 接雨水 文章…

Compute Express Link (CXL): An Open Interconnect for Cloud Infrastructure——论文阅读

DAC 2023 Paper CXL论文阅读笔记整理 背景 Compute Express Link是一种开放的行业标准互连&#xff0c;在PCI Express&#xff08;PCIe&#xff09;之上提供缓存和内存语义&#xff0c;具有资源池和织物功能。本文探讨了CXL在解决云基础设施中的一些挑战方面的作用。 CXL主要…

数据化运营09 抓住问题关键:用相关性分析拆解多个影响因素

前一讲&#xff0c;和你探讨了多维分析的方法&#xff0c;通过多维分析来寻找指标变化的原因。当我们找到问题的原因时&#xff0c;自然会进一步思考一个问题&#xff1a;指标变化的原因这么多&#xff0c;决定问题的关键因素又是哪个呢&#xff1f; 需要专栏原数据进行实操的同…

Linux小程序——进度条

前言&#xff1a;哈喽小伙伴们&#xff0c;经过我们对多个Linux基本开发工具的学习之后&#xff0c;对于Linux的使用也算是更上一层楼。 所以这篇文章&#xff0c;我们就尝试使用我们学过的Linux知识来写一个小程序——进度条&#xff0c;达到实践以及加深知识映像的效果。 目…

智能文档处理技术综述

一、 智能文档处理介绍 智能文档处理&#xff08;Intelligent Document Processing, IDP&#xff09;是利用人工智能&#xff08;AI&#xff09;、机器学习&#xff08;ML&#xff09;、计算机视觉&#xff08;CV&#xff09;、自然语言处理&#xff08;NLP&#xff09;等技术…

POJ3037 + HDU-6714

两道最短路好题 POJ3037 手玩一下 发现每一点的速度可以直接搞出来&#xff0c;就是pow(2,h[1][1]-h[i][j])*V 那么从这个点出发到达别的点的耗费的时间都是上面这个数的倒数&#xff0c;然后直接跑最短路就好了 #include<iostream> #include<vector> #include<…

BeanPostProcessors是什么以及如何使用?

目录 一、BeanPostProcessors是什么&#xff1f;二、如何使用 BeanPostProcessor1、实现 BeanPostProcessor 接口2、注册 BeanPostProcessor3、示例代码 三、使用场景四、注意事项 一、BeanPostProcessors是什么&#xff1f; BeanPostProcessor 是 Spring 框架提供的一个扩展点…

Java多线程实战-从零手搓一个简易线程池(一)定义任务等待队列

&#x1f3f7;️个人主页&#xff1a;牵着猫散步的鼠鼠 &#x1f3f7;️系列专栏&#xff1a;Java全栈-专栏 &#x1f3f7;️本系列源码仓库&#xff1a;多线程并发编程学习的多个代码片段(github) &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正…

每日一题——LeetCode1748.唯一元素的和

方法一 两次遍历 var sumOfUnique function(nums) {let map new Map()for(let num of nums){map.set(num,map.has(num)?map.get(num)1:1)}let res0for(let num of nums){if(map.get(num)1) resnum}return res }; 消耗时间和内存情况&#xff1a; 方法二 一次遍历 var su…
最新文章