项目常用的小功能实现——图片懒加载

195 阅读5分钟

写在前面

当我们需要在页面中显示许多图片时,如果直接使用img标签将图片显示,效果往往不好,特别是图片资源十分庞大的情况。

image.png

白屏就是这样来的,在解析HTML时,浏览器渲染主线程遇到img标签,会通知网络进程去加载资源,浏览器需要加载完所有图片并且构建了DOM树之后才能完成后续的渲染。

懒加载图片能够解决这个问题。现代应用都引入了成熟的懒加载技术。实现起来也很简单,按需加载即可,当图片进入视口范围,加载图片。

下面提供了三种方案,分别是:

  • HTML5 Data Attributes
  • 经典JS实现
  • IntersectionObserver API

实现方案

一、HTML5 Data Attributes

原生的HTML支持loading="lazy"属性,简单易用。

<img data-src="image.jpg" loading="lazy" >

添加一个loading属性即可,其中data-src是一个自定义属性。

MDN文档是这样描述的:data-*  全局属性 是一类被称为自定义数据属性的属性,它赋予我们在所有 HTML 元素上嵌入自定义数据属性的能力,并可以通过脚本在 HTML 与 DOM 表现之间进行专有数据的交换。

总结data-*能够让你在HTML中添加自定义数据,并且在JS中使用。

二、经典JS实现

JS框架能做的事,原生JS一定没问题。

主要分两块:

1. 基于数据属性的JS图片懒加载逻辑

        const imgs = document.getElementsByTagName('img');
        //需要下载的图片数
        const num = imgs.length;
        let n = 0;
        function loadImages() {

            //是否在可视区域
            //一屏的高度
            let screenHeigth  = document.documentElement.clientHeight;
            //不同的浏览器的兼容性问题 判断前一个是否生效,生效则不处理后一个
            let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
            for(let i = 0 ; i < num ; i++){
                if(imgs[i].offsetTop < screenHeigth + scrollTop){
                    imgs[i].src = imgs[i].getAttribute('data-src');
                    n = i + 1;
                    if( n === num ){
                        console.log("所有图片加载完毕!");
                        //移除监听,加入节流后监听函数改变
                        window.removeEventListener('scroll',throttleLayLoad);
                    }
                }
            }
        }

2. 节流手段的优化(这里引入了lodash.js)

    <script>
        //节流,设置检测时间
        const throttleLayLoad = _.throttle(loadImages,200);
        //初始化一次,再页面加载之后 DOM树加载完毕
        document.addEventListener('DOMContentLoaded',loadImages);
        window.addEventListener('scroll',throttleLayLoad)
    </script>   

三、IntersectionObserver API

IntersectionObserver意为交叉观察器,是一种异步检测被观察元素和视口的交叉情况。

用法

创建一个交叉观察器,提供一个回调函数和选项对象:

let observer = new IntersectionObserver(callback, options);

当第一次监听目标元素时,该回调函数会触发。

当目标元素和设备视口或者指定区域相交,该回调函数也会触发。

let options = {
  root: document.querySelector("#scrollArea"),
  rootMargin: "0px",
  threshold: 0.5,
};

选项中,root表示用做视口的元素,且必须是待观察元素的祖先元素,默认为浏览器视口。rootMargin表示视口或者指定元素的边距,类似于CSS的margin属性,默认为0。threshold用于描述目标可见度到达多少时,回调函数执行。

按照上面的例子:

   root: document.querySelector("#scrollArea"),

说明是以idscrollArea的元素作为视口。

  rootMargin: "0px",

视口元素边距为0

  threshold: 0.5,

在被观察元素可见度达到50%后触发回调函数。

image.png

具体实现

先看看接下来要用到的数据:

      <img
        class="lazy"
        data-src="https://img.36krcdn.com/hsossms/20240722/v2_b4acb4d77d28416c926560369ea9bbb4@5888275_oswg576270oswg1053oswg495_img_png?x-oss-process=image/resize,m_mfit,w_600,h_400,limit_0/crop,w_600,h_400,g_center/format,webp"
      />

lazy作为类名,依旧使用了自定义数据属性data-src,这是图片懒加载的常用手段。

数据格式很简单,主要还是对观察器的配置和回调函数的实现。

1. 获取到所有图片数据

const images = querySelectorAll('.lazy');

2. 创建观察器

    //entries作为形参,后续观察时将images传入即可
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            //目标元素是否与观察区域有交集
            if(entry.isIntersecting){
                loadImage(entry.target);
            }
        })
    },{
        //待添加的配置
    })

3. loadImage函数

const loadImage = (image) => {
    //将自定义属性的值赋给src,加载资源
    image.src = image.dataset.src;
    //后续不需要懒加载,已经在缓存中
    image.classList,remove("lazy");
}

4. 遍历图片元素观察

//为观察器添加元素,这里表示所有图片都需要被观察
images.forEach(image => {
    observer.observe(image);
})

这里的observe是构造函数IntersectionObeserver的一个实例对象,observe()是一个添加待观察元素的方法。

5. 图片加载完毕,关闭观察器;添加配置

    //entries作为形参,后续观察时将images传入即可
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            //目标元素是否与观察区域有交集
            if(entry.isIntersecting){
                loadImage(entry.target);
                //取消观察
                observer.unobserve(entry.target);
            }
        })
    },{

      rootMargin: "0px",
      threshold: 0.5,
    })

6. 添加一个监听事件,确保不会访问到DOM结构未加载完成的元素。

    <script>
      document.addEventListener('DOMContentLoaded' , () => {
        //所有逻辑
      })
    </script>

效果如下,

lazy.gif

优点

1. 事件驱动机制

IntersectionObserver 是基于事件驱动的。这意味着它只在元素与视口或其他元素的相交状态发生改变时才触发回调。如果使用 getBoundingClientRect() 并结合定时器(setInterval)检测元素相交情况,那么无论是否相交都需要不停检查,极大浪费了性能。

2. 避免重绘和重排

当使用 getBoundingClientRect() 检查元素的位置时,浏览器会重新计算元素的布局信息,这会导致重排和重绘。而 IntersectionObserver 是异步的,其回调函数在主线程空闲时执行。特别的,如果不在回调函数内操作DOM元素,则不会触发重排重绘,这在处理大量元素时更加高效。

3. 自动化管理

IntersectionObserver 能够自动地管理多个被观察元素的状态。一旦创建了一个观察者实例并指定了要观察的元素,浏览器就会自动跟踪这些元素的相交情况。当相交状态达到配置标准时,回调函数就会执行。

总结

许多场景需要检测两个元素是否相交:

  • 在页面滚动时“懒加载”图像或其他内容。
  • 实现“无限滚动”网站,在滚动过程中加载和显示越来越多的内容,这样用户就不必翻页了。
  • 报告广告的可见度,以便计算广告收入。
  • 根据用户是否能看到结果来决定是否执行任务或动画进程。

如果我们想要自己实现一个功能,涉及到检测元素相交的情况。从性能角度考虑,使用IntersectionObserver是一个不错的选择。