图片懒加载的实现分析

460 阅读7分钟

前言

在我们日常的性能优化里,图片懒加载是一个很常规的操作,它的作用可以理解为用户需要这个资源,才会去加载。市面上有很多现成的库,不过理解其工作原理非常重要,这样可以提升我们的综合能力。

什么是图片懒加载?

在手机或电脑里,屏幕的大小是固定的,这也决定了用户能看到的视图范围有限;假设页面上一个列表有100+张图片,大部分图片不在可视区域内,所以不在可视区域内的图片可以晚点再加载,比如用户滚动就快到看到前加载即可。

懒加载实现原理

把img元素的src属性设置为一个较小的图片链接,把真正要加载的图片链接放到data-src里,然后我们监听元素进入可视区域,再把真正要加载的链接设置到src里。如下:

<!-- 元素进入区域前 -->
<img src="thumbnail.jpg" data-src="original.jpg">
<!-- 元素进入区域后 -->
<img src="original.jpg" data-src="original.jpg">

IntersectionObserver实现监听

官方定义:Intersection Observer API 允许你配置一个回调函数,每当目标(target)元素与设备视窗或者其他指定元素发生交集的时候执行。设备视窗或者其他元素我们称它为根(root)。通常,您需要关注文档最接近的可滚动祖先元素的交集更改,如果元素不是可滚动元素的后代,则默认为设备视窗。如果要观察相对于根(root)元素的交集,请指定根(root)元素为null

IntersectionObserver 目前已经被主流的浏览器支持(IE:你们看着我干甚么???),大可放心食用。

用例:

// 创建一个intersectionObserver对象
var intersectionObserver = new IntersectionObserver(callback, options)
// 开始监听某个元素,要监听多个元素需要多次调用。
intersectionObserver.observe(document.getElementById(img))

options是配置参数,主要有3个属性组成

  • root:所监听对象的具体祖先元素。如果未传入值或值为null时,即代表设备视窗。
  • rootMargin:计算交叉时添加到根(root)边界盒的矩形偏移量,默认为:0px
  • threshold:一个数组,比如,设置[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。

callback回调函数接收一个ertries参数,它是一个数组,每一项代表在这次回调中,被监听的元素可见性发生了变化;每个成员都是一个IntersectionObserverEntry描述了目标元素与root的交叉关系。我们实现图片懒加载暂时只需要关注2个参数。

  • isIntersecting:返回一个布尔值。如果目标元素出现在root可视区,返回true。2. 如果从root可视区消失,返回false。
  • target:监听的元素。

代码实现

<body>
  <div>
    <img class="lazyLoad" src="./image/3.jpg" data-src="./image/2.jpg" >
    <img class="lazyLoad" src="./image/3.jpg" data-src="./image/2.jpg" >
    <img class="lazyLoad" src="./image/3.jpg" data-src="./image/2.jpg" >
    <img class="lazyLoad" src="./image/3.jpg" data-src="./image/2.jpg" >
    <img class="lazyLoad" src="./image/3.jpg" data-src="./image/2.jpg" >
  </div>
  <script>
    var images = document.querySelectorAll('.lazyLoad')
    var IntersectionObserver = new IntersectionObserver((ertries) => {
      ertries.forEach(entry => {
        // 当元素进入可视区内
        if (entry.isIntersecting) {
          let src = entry.target.getAttribute('data-src')
          if ('img' === entry.target.tagName.toLowerCase()) {
            if (src) {
              entry.target.src = src
            }
          }
        }
      })
    })
    Array.from(images).forEach(image => {
      IntersectionObserver.observe(image)
    })
  </script>
</body>

效果:初始化的时候已经有部分图片在可是区域内了,所以会直接显示需要加载的图片,当我们滑动页面时,注意看下图右侧元素菜单,发现后面2个img元素的src变动了。这就已经简单实现了我们的图片懒加载功能。

图片显示会给你感觉没有懒加载的效果,是因为笔者浏览器有缓存,加载较快导致,但是不影响实际的原理实现。

getBoundingClientRect实现监听

该方法返回一个 DOMRect 对象,是一个包含了一组用于描述边框的只读属性——left、top、right和bottom,单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的。

我们有了这些数据,只要判断目标元素是否在可视区内就可以了,下面我们先来实现这个方法。

function isInViewport (el) {
  const { top, bottom } = el.getBoundingClientRect()
  // 为true则代表元素顶部小于窗口高度
  const below = top < window.innerHeight ? true : false
  // 为true代表元素的底部在可视区内
  const above = bottom > 0 ? true : false
  return below && above ? true : false
}

上面的代码判断了元素是否在可视区内,那么现在我们只要监听滚动事件,遍历所有的元素,判断它们是否在可视区内就可以了。

var images = document.querySelectorAll('.lazyLoad')

function lazyLoad () {
  for (let i = 0; i < images.length; i++) {
    let target = images[i]
    if (isInViewport(target)) {
      let src = target.getAttribute('data-src')
      if ('img' === target.tagName.toLowerCase()) {
        if (src) {
          target.src = src
        }
      }
    }
  }
}
// 初始化先执行一次
lazyLoad()
document.addEventListener('scroll', lazyLoad);
document.addEventListener('resize', lazyLoad);

当然,想要优化好再加一个节流函数即可。

lazyload.js源码实现

上面我们已经简单的实现了图片懒加载功能,接下来我们来分析lazyload.js实现的源码,

lazyload.js的实现核心部分只有100多行的代码,下面先看一下核心逻辑。

const defaults = {
  src: "data-src", // 代表要真正加载的图片
  srcset: "data-srcset", // srcset是响应式的图片,浏览器根据不同手机像素比,加载不同像素的图片
  selector: ".lazyload", // 默认需要懒加载元素的选择器
  root: null,
  rootMargin: "0px",
  threshold: 0
};

function LazyLoad(images, options) {
  // 默认配置和options的合并
  this.settings = extend(defaults, options || {});
  // 获取需要懒加载的标签元素
  this.images = images || document.querySelectorAll(this.settings.selector);
  this.observer = null;
  // 初始化
  this.init();
}

上面代码是当我们执行new LazyLoad函数后的初始化操作,主要有三部分逻辑:

  1. 把内置的defaults和传入的options做合并,这个函数不在这里分析。
  2. 获取需要懒加载的元素列表。
  3. 初始化IntersectionObserver监听。

核心init函数分析

function init() {

  // 如果浏览器不支持IntersectionObserver,则直接加载全部图片。
  /* Without observers load everything and bail out early. */
  if (!root.IntersectionObserver) {
    // 先忽略
    this.loadImages();
    return;
  }

  let self = this;
  let observerConfig = {
    root: this.settings.root,
    rootMargin: this.settings.rootMargin,
    threshold: [this.settings.threshold]
  };

  this.observer = new IntersectionObserver(function(entries) {
    // 遍历所有发生变化的元素
    Array.prototype.forEach.call(entries, function (entry) {
      // 当元素进入可视区内
      if (entry.isIntersecting) {
        // 把该元素取消监听,因为当元素出现在可视区内,就无需再观测了。
        self.observer.unobserve(entry.target);
        // 获取要加载的图片
        let src = entry.target.getAttribute(self.settings.src);
        // 获取要加载的响应式的图片
        let srcset = entry.target.getAttribute(self.settings.srcset);
        if ("img" === entry.target.tagName.toLowerCase()) {
          if (src) {
            entry.target.src = src;
          }
          if (srcset) {
            entry.target.srcset = srcset;
          }
        } else {
          // 对于其他元素,替换它的背景图
          entry.target.style.backgroundImage = "url(" + src + ")";
        }
      }
    });
  }, observerConfig);

  // 遍历this.images,监听所有目标元素
  Array.prototype.forEach.call(this.images, function (image) {
    self.observer.observe(image);
  });
}

上面的代码首先判断了浏览器是否支持IntersectionObserver方法,如果不支持则执行loadImages方法加载所有图片;接下来初始化 IntersectionObserver 函数;然后遍历所有目标元素,执行 observe 的监听。

当元素进入可视区内时,会触发回调方法,然后再遍历变动的目标元素,修改它们对应的属性即可。

其他函数

function loadImages () {
  if (!this.settings) { return; }

  let self = this;
  Array.prototype.forEach.call(this.images, function (image) {
    let src = image.getAttribute(self.settings.src);
    let srcset = image.getAttribute(self.settings.srcset);
    if ("img" === image.tagName.toLowerCase()) {
      if (src) {
        image.src = src;
      }
      if (srcset) {
        image.srcset = srcset;
      }
    } else {
      image.style.backgroundImage = "url('" + src + "')";
    }
  });
}

function destroy () {
  if (!this.settings) { return; }
  // 关闭观察
  this.observer.disconnect();
  this.settings = null;
}

loadImages 函数与上面分析的基本一致,destroy函数用来摧毁观察。

注意

  1. 也可以使用imgEle.offsetTop < window.innerHeight + document.body.scrollTop,观察图片是否进入可视区内实现,不过方法比较low,就没有去实现了。
  2. 监听浏览器scroll事件去实现懒加载,是无法在 better-scroll 这种库使用的。但是用 IntersectionObserver 实现的这种方式是不受影响。

感谢观看我的文章,如果对您有帮助,请点个赞👍~

参考

Intersection Observer API

getBoundingClientRect

github.com/tuupola/laz…