图片懒加载原理

本文最后更新于:2022年4月22日 上午

通过 getBoundingClientRectIntersectionObserver 分别实现图片懒加载

明确目标

我们先写一个基本的页面,让 图片 超出窗口的高度,然后我们改写成当滚动到图片标签位置时再向服务器请求资源

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>图片懒加载原理</title>
  <style>
    html,
    body {
      height: 3000px;
    }

    section {
      width: 600px;
      height: 300px;
      margin: 1500px auto 0;    // 让容器距离顶部有一段距离,需要滚动屏幕才能出现
      background-color: #ccc;
    }

    section img {
      width: 100%;
      height: 100%;
      opacity: 0;      // 默认透明度为0
      transition: opacity 1s;
    }
  </style>
</head>

<body>

  <section>
    <img src="" alt="" />
  </section>

</body>

</html>

可以看到当前我们的布局,需要向下滚动一定的距离,才能到达 img 标签所在的位置

getBoundingClientRect方式实现

getBoundingClientRect 可以用来获取某个元素相对于窗口的 topleftrightbottom

getBoundingClientRect 的基本使用

const box = document.getElementById("box")
const { top, left, right, bottom } = box.getBoundingClientRect();

getBoundingClientRect + onscroll

一般我们都能想到使用 需要用到监听屏幕滚动,采用监听屏幕滚动的方式具体方案如下

  1. 获取 可视窗口的高度

  2. 监听浏览器滚动

  3. 滚动触发时获取 img 标签距离顶部的距离,当距离顶部的距离小于可视窗口高度时,说明现在 ** img 标签已经进入了可视范围** ,可以开始正式加载图片了

如下图所示

新增自定义属性

先给 img 标签新增一个自定义属性 data-src 将我们真实需要加载的图片地址放在 data-src

<img 
  src="" 
  alt=""
  data-src="https://img1.baidu.com/it/u=588478138,900370032&fm=26&fmt=auto&gp=0.jpg"
>

获取到 可视窗口高度 和 img 标签

const img = document.querySelector('img');
const clientHeight = document.documentElement.clientHeight;      // 获取窗口可视区域高度

封装 图片懒加载 函数

img 添加一个 isLoad 用来标识图片是否已经加载过,

  • 加载过

&ensp;&ensp;&ensp;&ensp;如果加载过则 return

  • 未加载过

&ensp;&ensp;&ensp;&ensp;- 从 data-src取到图片地址赋值给src属性,

&ensp;&ensp;&ensp;&ensp;- 监听图片onload事件,触发后将opacity设置为 1,并且将 isLoad 标识改为 true

&ensp;&ensp;&ensp;&ensp;

function lazyImgLoad() {
  if (img.isLoad) return;
  const src = img.getAttribute('data-src');
  img.src = src;
  img.onload = () => {
    img.style.opacity = 1;
    img.isLoad = true;
  }
}

监听 屏幕滚动

window.onscroll = function () {
const { top } = img.getBoundingClientRect();    // 获取元素的位置信息

// 判断距离顶部的位置是否小于可视窗口区域,如果小于则调用 懒加载函数
if (top <= clientHeight) lazyImgLoad();

}

基本实现

现在我们的懒加载已经基本实现了,打开浏览器的 network 可以看到,当我们第一次进入页面的时候,是不会请求图片的,当滚动到图片区域的时候,才开始发送请求

优化

我们会发现,我们上面的代码,每当我们滚动的时候,都会触发 onscroll 监听函数,浏览器会在最快的时间触发监听函数,一般为 5ms - 7ms 之间,这个频率很明显太快了。

这里我们可以用节流来避免短时间内频繁触发来优化一下性能

function throttle(fn, delay = 500) {
  let time;
  return (...args) => {
    if (time) return;                     // 节流
    time = setTimeout(() => {
      time = null;                        // 节流
      fn.apply(null, args)
    }, delay);
  }
}
window.onscroll = throttle(function (e) {
  const { top } = img.getBoundingClientRect();    // 获取元素的位置信息

  if (top <= clientHeight) lazyImgLoad();

},100)

IntersectionObserver 方式实现

虽然上述使用 节流来优化了一下性能,但是也牺牲快速响应,所以这里我们可以来使用浏览器自带的 IntersectionObserver ,它可以用来异步观察元素(支持多个)是否在可视窗口内,这个 api 除了 IE,其它浏览器都兼容。

IntersectionObserver 基本使用

语法也比较简单,第一个参数是回调函数,当元素与窗口交叉的时候会触发,第二个参数是 options 参数,这个参数非必传

这个函数会返回一个观察器的实例,可以通过它来操作需要观察哪个 dom 元素

const observe = IntersectionObserver(callback, options)

创建监听对象

const ob = new IntersectionObserver(entries => {
  console.log(entries[0]);
}, {
  threshold: [1],   // 表示完全出现时会触发一次回调
});
// entrie 对象结构如下
{
  boundingClientRect: DOMRectReadOnly {x: 101, y: 479, width: 600, height: 300, top: 479, …}
  intersectionRatio: 1
  intersectionRect: DOMRectReadOnly {x: 101, y: 479, width: 600, height: 300, top: 479, …}
  isIntersecting: true      // 表示容器是否当前是否可见
  isVisible: false
  rootBounds: DOMRectReadOnly {x: 0, y: 0, width: 802, height: 782, top: 0, …}
  target: img
  time: 1428.300000011921
}

添加回调处理函数

const ob = new IntersectionObserver(entries => {

  const { isIntersecting, target } = entries[0];

  if (isIntersecting) {   // 当处于可见时,开始加载图片,且停止监听当前目标
    lazyImgLoad();
    ob.unobserve(target);
  }

}, {
  threshold: [1],   
});

监听容器

ob.observe(img);

实现

可以看到,完美实现,且不需要手动做其它优化,目前基本上的ui库图片懒加载的原理都与此类似

最后附上完整代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>图片懒加载原理</title>
  <style>
    html,
    body {
      height: 3000px;
    }

    section {
      width: 600px;
      height: 400px;
      margin: 1500px auto 0;
      background-color: #ccc;
    }

    section img {
      width: 100%;
      height: 100%;
      opacity: 0;
      transition: opacity 1s;
    }
  </style>
</head>

<body>

  <section>
    <img src="" alt=""
      data-src="https://img1.baidu.com/it/u=588478138,900370032&fm=26&fmt=auto&gp=0.jpg">
  </section>

  <script>
    // 图片懒加载是当图片进入可视区域的时候才开始加载
    // 监听图片顶部距离窗口顶部的距离,如果图片顶部具体窗口顶部的距离小于 窗口的高度,则表示,图片进入了可视区域

    const img = document.querySelector('img');
    const clientHeight = document.documentElement.clientHeight;      // 获取窗口可视区域高度

    function lazyImgLoad() {
      if (img.isLoad) return;
      const src = img.getAttribute('data-src');
      img.src = src;
      img.onload = () => {
        img.style.opacity = 1;
        img.isLoad = true;
      }
    }

    // 使用 onscroll 实现
    // window.onscroll = throttle(function (e) {
    //   const { top } = img.getBoundingClientRect();    // 获取元素的位置信息

    //   if (top <= clientHeight) lazyImgLoad();

    // })

    function throttle(fn, delay = 500) {
      let time;
      return (...args) => {
        // if (time) clearTimeout(time);      // 防抖
        if (time) return;                     // 节流
        time = setTimeout(() => {
          time = null;                        // 节流
          fn.apply(null, args)
        }, delay);
      }
    }

    // 优化
    // 通过浏览器的 IntersectionObserver 来帮助我们监听,加强性能
    // 但是兼容 `IE`, 需要兼容 IE 的尽早逃吧,不要把有限的时间浪费在无意义的事情上。
    // 创建一个监听对象
    const ob = new IntersectionObserver(change => {
      console.log(change[0]);
      /* 
        boundingClientRect: DOMRectReadOnly {x: 101, y: 479, width: 600, height: 300, top: 479, …}
        intersectionRatio: 1
        intersectionRect: DOMRectReadOnly {x: 101, y: 479, width: 600, height: 300, top: 479, …}
        isIntersecting: true      // 表示容器是否当前是否可见
        isVisible: false
        rootBounds: DOMRectReadOnly {x: 0, y: 0, width: 802, height: 782, top: 0, …}
        target: img
        time: 1428.300000011921
       */

      const { isIntersecting, target } = change[0];

      if (isIntersecting) {
        lazyImgLoad();
        ob.unobserve(target);
      }

    }, {
      threshold: [1],   // 表示完全出现时会触发一次回调
    });

    // 调用 observe 方法,将 img 传入,表示监听 img 元素
    ob.observe(img);

    // console.log(ob);
  </script>
</body>

</html>

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处。