图片懒加载原理
本文最后更新于:2022年4月22日 上午
通过 getBoundingClientRect 和 IntersectionObserver 分别实现图片懒加载
明确目标
我们先写一个基本的页面,让 图片 超出窗口的高度,然后我们改写成当滚动到图片标签位置时再向服务器请求资源
<!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 可以用来获取某个元素相对于窗口的 top,left,right,bottom 值
getBoundingClientRect 的基本使用
const box = document.getElementById("box")
const { top, left, right, bottom } = box.getBoundingClientRect();getBoundingClientRect + onscroll
一般我们都能想到使用 需要用到监听屏幕滚动,采用监听屏幕滚动的方式具体方案如下
获取 可视窗口的高度
监听浏览器滚动
滚动触发时获取
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 用来标识图片是否已经加载过,
- 加载过
 
    如果加载过则 return
- 未加载过
 
    - 从 data-src取到图片地址赋值给src属性,
    - 监听图片onload事件,触发后将opacity设置为 1,并且将 isLoad 标识改为 true
    
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 协议,转载请注明出处。