专栏名称: 前端大全
分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯
目录
相关文章推荐
前端充电宝  ·  前端的本质 ·  3 天前  
前端充电宝  ·  前端的本质 ·  3 天前  
前端早读课  ·  【早阅】使用 Fabric.js v6 ... ·  3 天前  
前端早读课  ·  【招聘】上海Devv.AI招前端工程师 ·  6 天前  
前端早读课  ·  【第3372期】在 Node.js 中使用 ... ·  6 天前  
51好读  ›  专栏  ›  前端大全

无限滚动加载数据利器:IntersectionObserver

前端大全  · 公众号  · 前端  · 2024-09-05 10:00

正文


作者:Aplee

https://juejin.cn/post/7389077092137517108

今天和同事讨论时,讨论了页面滚动加载数据的事情,正好我去年年底也做过相同的功能,只是当时因为各种原因吧,没有做总结。现在回想起来,只是记得以前做过,在哪个页面实现的,具体实现的方法,确实有点忘记了。记得当时好像也尝试了很多的方法,最后才实现,这里又要吐槽一下自己的笨了。

所以把当时的代码给扒出来,是利用IntersectionObserver这个api实现的,然后在网上找了一些资料,准备输出整理一下相关的文档。

好,我们正式开始介绍。

介绍

IntersectionObserver 是一种现代 Web API,它允许开发者异步观察一个目标元素与其祖先元素或视口(viewport)交叉状态的变化。这对于实现图片懒加载或无限滚动功能非常有用。

这里我们借用阮一峰大佬的图片介绍:

图片来源为:IntersectionObserver API 使用教程[1]

传统的实现方法是,在监听到scroll事件后,调用目标元素(绿色方块)的getBoundingClientRect()方法,获取它相对于视口左上角的坐标,然后判断是否在视口内。然而,这种方法的缺点在于由于scroll事件频繁触发,计算量较大,容易导致性能问题。

而现在就可以使用IntersectionObserver 这个api实现。

具体实现见下文。

API

IntersectionObserver API是一个用于异步监听目标元素与其祖先或视口(viewport)交叉状态的API。它可以有效地观察页面上的元素,特别是在需要实现懒加载(lazy loading)、无限滚动(infinite scrolling)或者特定动画效果时非常有用。

在介绍IntersectionObserver API之前,我们先介绍一些概念,便于在后面使用。

  1. 目标元素(Target Element) :需要被观察交叉状态的DOM元素。
  2. 根元素(Root Element) :IntersectionObserver的根元素,即用来定义视口的边界。如果未指定,默认为浏览器视口。
  3. 交叉状态(Intersection) :目标元素与根元素或视口相交的部分。可以通过IntersectionObserver的回调函数获取交叉状态的详细信息。
  4. 阈值(Threshold) :一个介于0和1之间的值,用来指定目标元素什么时候被视为“交叉”。例如,一个阈值为0.5表示当目标元素50%可见时触发回调。

基本使用

使用 IntersectionObserver API 的基本步骤如下:

  1. **创建一个IntersectionObserver对象**:
let observer = new IntersectionObserver(callback, options);
  • callback 是一个回调函数,当目标元素与根元素(或视口)交叉状态发生变化时被调用。
  • options 是一个配置对象,用于设置观察选项。
  1. **定义一个回调函数,用于处理元素与视窗的交叉状态变化**:
let callback = (entries, observer) => {
  entries.forEach(entry => {
    // 处理交叉状态变化
    if (entry.isIntersecting) {
      // 元素进入视窗
    } else {
      // 元素离开视窗
    }
  });
};
  • entries 是一个包含所有被观察目标元素的 IntersectionObserverEntry 对象数组。
  • observer 是调用回调函数的 IntersectionObserver 实例。
  1. **指定要观察的目标元素,并开始观察**:
let targetElement = document.querySelector('.target-element');
observer.observe(targetElement);
  • targetElement 是要观察的目标元素。可以通过选择器、getElementById 等方法获取。
  1. **可选:配置IntersectionObserver的行为,包括根元素、根元素的边界和交叉比例的阈值等属性**:
let options = {
  rootnull// 观察元素的根元素,null表示视窗
  rootMargin'0px'// 根元素的边界
  threshold0.5 // 交叉比例的阈值,0.5表示元素一半进入视窗时触发回调
};
  1. **在回调函数中处理元素的交叉状态变化,根据需要执行相应的操作**。
  2. 停止观察元素(可选)
observer.unobserve(targetElement);
  1. 停止观察所有元素并清除所有观察者(可选)
observer.disconnect();

通过以上步骤,您可以使用IntersectionObserver API来监测元素与视窗的交叉状态,并根据需要执行相应的操作,实现一些常见的交互效果和性能优化。

// 开始观察
io.observe(document.getElementById('example'));

// 停止观察
io.unobserve(element);

// 关闭观察器
io.disconnect();

IntersectionObserver 构造函数 实例返回方法

当你使用 IntersectionObserver 构造函数创建一个观察器实例后,这个实例会携带四个主要方法来管理和操作观察的行为。这些方法包括:

  1. observe(targetElement)

  • 用于开始观察指定的目标元素。一旦目标元素进入或离开视口,观察器就会触发回调函数。
  • unobserve(targetElement)

    • 用于停止观察指定的目标元素。当你不再需要观察某个元素时,可以使用该方法来取消观察。
  • disconnect()

    • 用于停止观察所有目标元素,并将观察器从所有目标元素中移除。当你不再需要观察任何元素时,可以使用该方法来完全关闭观察器。
  • takeRecords()

    • 用于获取当前观察器实例尚未处理的所有交叉记录(Intersection Records)。该方法返回一个数组,包含所有未处理的交叉记录对象。每个交叉记录对象表示一个目标元素与根元素(或根元素的可视窗口)的交叉信息。

    这些方法可以帮助你控制 IntersectionObserver 实例的行为,以及对特定元素进行观察和取消观察。

    // 开始观察
    io.observe(document.getElementById('example'));

    // 停止观察
    io.unobserve(element);

    // 关闭观察器
    io.disconnect();

    // 返回所有观察目标的 IntersectionObserverEntry 对象数组
    io.takeRecords();

    entries介绍

    entries 是传递给 IntersectionObserver 回调函数的一个参数,它是一个包含 IntersectionObserverEntry 对象的数组。每个 IntersectionObserverEntry 对象表示一个被观察目标元素的最新交叉状态(即目标元素与根元素或视口的交集)。

    IntersectionObserverEntry 对象

    每个 IntersectionObserverEntry 对象包含了以下重要信息:

    1. time

    • 一个时间戳,表示交叉状态变化的时间戳,精确到毫秒,通常用于性能测量。
  • target

    • 被观察的目标元素,即观察器正在观察的具体 DOM 元素。
  • rootBounds

    • 一个 DOMRectReadOnly 对象,表示根元素的边界框。如果根元素是视口,则表示视口的边界框。
  • boundingClientRect

    • 一个 DOMRectReadOnly 对象,表示目标元素的边界框,即元素在视口中的位置和尺寸。
  • intersectionRect

    • 一个 DOMRectReadOnly 对象, 表示目标元素与视口(或指定的根元素)交叉部分的矩形区域的信息。如果没有交叉,它的值为 {top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0}
  • intersectionRatio

    • 一个数值,表示目标元素可见部分占自身的比例,取值范围在 0.01.0 之间。当元素完全不可见时,值为 0.0;当元素完全可见时,值为 1.0
  • isIntersecting

    • 一个布尔值,表示目标元素当前是否与视口(或指定的根元素)交叉。如果元素至少部分可见,则为 true;否则为 false

    以下是一个简单的示例,演示了如何在 IntersectionObserver 的回调函数中处理 entries 数组:

    // 创建 IntersectionObserver 实例
    let observer = new IntersectionObserver(callback, options);

    // 观察目标元素
    let target = document.querySelector('.lazy-load');
    observer.observe(target);

    // 回调函数
    function callback(entries, observer{
      entries.forEach(entry => {
        // 输出目标元素的交叉状态信息
        console.log('Target:', entry.target);
        console.log('Time:', entry.time);
        console.log('Bounding Client Rect:', entry.boundingClientRect);
        console.log('Intersection Rect:', entry.intersectionRect);
        console.log('Intersection Ratio:', entry.intersectionRatio);
        console.log('Is Intersecting:', entry.isIntersecting);

        // 根据交叉状态执行相应操作
        if (entry.isIntersecting) {
          // 如果元素进入视口,加载内容或执行其他操作
          loadContent(entry.target);
          observer.unobserve(entry.target); // 停止观察已加载的元素
        }
      });
    }

    function loadContent(element{
      // 加载内容的具体实现
    }

    注意事项

    • 多目标处理: IntersectionObserverEntry 对象在数组中返回,因此你可以同时处理多个目标元素的交叉状态变化。
    • 性能优化: 使用 intersectionRatio 属性可以帮助优化性能,因为你可以根据元素的可见性决定何时加载或操作内容,而无需频繁地检查元素位置或滚动事件。
    • 观察器管理: 在回调函数中,你可以通过 observer.unobserve(entry.target) 来停止观察已经处理过的元素,避免不必要的性能损耗。

    应用

    IntersectionObserver 特别适用于懒加载图像、无限滚动内容、以及其他需要根据元素可见性触发的操作。

    图片懒加载

    当页面上有大量图片或其他资源需要加载时,可以使用IntersectionObserver来延迟加载这些资源。只有当图片进入视窗时才加载图片,可以提高页面加载性能和用户体验。

    下面是一个实现图片懒加载功能的 React 组件 LazyLoadImage

    import { useEffect, useRef, useState } from 'react';

    const LazyLoadImage = ({ src, alt, className }) => {
        const imgRef = useRef();
        const [isLoaded, setIsLoaded] = useState(false);

        useEffect(() => {
            const observer = new IntersectionObserver(
                entries => {
                    entries.forEach(entry => {
                        if (entry.isIntersecting) {
                            const img = entry.target;
                            img.src = src;
                            img.onload = () => setIsLoaded(true);
                            observer.unobserve(img);
                        }
                    });
                },
                { threshold0.1 }
            );

            const imgElement = imgRef.current;
            if (imgElement) {
                observer.observe(imgElement);
            }

            return () => {
                if (imgElement) {
                    observer.unobserve(imgElement);
                }
            };
        }, [src]);

        return (
            <img
                ref={imgRef}
                alt={alt}
                className={className}
                style={{
                    opacity: isLoaded ? 1 : 0.5,
                    transition: 'opacity 0.5s ease-in-out',
                }}
            />

        );
    };

    export default LazyLoadImage;

    该组件使用 useEffectIntersectionObserver 来检测图片是否进入视口,并在图片进入视口时加载图片资源。图片的透明度从 0.5 渐变为 1,实现渐显效果。

    无限滚动加载数据

    当用户滚动到页面底部时,自动加载更多数据,以实现无限滚动效果。

    import { useEffect, useState, useRef } from 'react'

    interface NewsItem {
    id: number
    title: string
    content: string
    }

    const fetchNews = async (lastId: number): Promise => {
    // 模拟从API获取新闻数据
    return new Promise((resolve) => {
    setTimeout(() => {
    const newsItems: NewsItem[] = []
    // 设置总共169条数据,后面展示暂无数据
    const ten = lastId > 150 ? 9 : 10
    for (let i = lastId + 1; i <= lastId + ten; i++) {
    newsItems.push({ id: i, title: `News ${i}`, content: `Content of News ${i}` })
    }
    resolve(newsItems)
    }, 1000)
    })
    }

    const InfiniteNewsList: React.FC = () => {
    const [news, setNews] = useState([])
    const [isLoading, setIsLoading] = useState(false)
    const [noMoreData, setNoMoreData] = useState(false)
    const loaderRef = useRef(null)

    useEffect(() => {
    const observer = new IntersectionObserver(
    (entries) => {
    if (entries[0].isIntersecting && !isLoading && !noMoreData) {
    const lastNews = news[news.length - 1]
    if (lastNews || news.length === 0) {
    setIsLoading(true)
    fetchNews(lastNews?.id || 0).then((newNews) => {
    if (newNews.length < 10) {
    setNoMoreData(true) // 如果返回的新闻少于10条,则没有更多数据
    }
    setNews([...news, ...newNews])
    setIsLoading(false)
    })
    }
    }
    },
    {
    root: null,
    rootMargin: '0px',
    threshold: 1.0
    }
    )

    if (loaderRef.current) {
    observer.observe(loaderRef.current)
    }

    return () => {
    if (loaderRef.current) {
    observer.unobserve(loaderRef.current)
    }
    }
    }, [isLoading, news, noMoreData])

    return (

    {!news.length &&
    暂无数据
    }
    {news.map((item) => (
    key={item.id}
    style={{ width: '20%', backgroundColor: '#999', padding: '10px', marginBottom: '10px', color: '#fff' }}
    >

    {item.title}


    {item.content}



    ))}
    {!noMoreData &&
    }
    {isLoading &&
    Loading more news...
    }
    {noMoreData &&
    没有更多数据了
    }