图片懒加载原理及手撸实现

544 阅读3分钟

概念

图片懒加载,就是在一开始的时候,收集需要做懒加载的图片元素集合。这些图片一般一开始都没有 src 属性,而是只有一个 data-src 自定义属性。对于这个图片元素的集合,通过判断哪些在视口内,哪些在视口外。在视口内的,就给它们的 src 属性赋值。

设置为"data-*"的格式,是因为只有这种格式才能在DOM中的dataset属性轻松取到,比如:data-name="zhang",在对应的DOM中其dataset为 { name: 'zhang' }。

根据现代浏览器的支持程度不同,这里可以有两种解决方案:

  • 针对现代浏览器,使用强大的IntersectionObserver(交叉观察器),不限容器、不限事件
  • 针对 IE 浏览器,以及一些版本古老的浏览器,使用getBouningClientRect方法+scroll事件+throttle节流器。

其中第二种降级方案的缺点是不言而喻的,比如:

  • 容器受限:当父级容器很小时,超出父级容器的图片通过 getBoundingClientRect 拿到的 top 小于视口高度,但是其实此时这张图片是不可见的。
  • 事件受限:如需应对不同的事件(滚动、缩放等),只能一个个添加事件监听器。

现代版本,强大的 IntersectionObserver

这个东西着实牛逼,只要给相关元素添加了观察器,无论是任何容器、任何事件、任何场合都能准确地判断用户“能不能看到它”,从 chrome51+版本就开始支持了。

它的使用流程是这样的:

  1. 实例化一个观察者对象,在实例化时传入可见性变化的回调作为参数;
  2. 通过.obeserve方法给 DOM 元素添加可见性监听器;
  3. 通过.unobserve方法移出对相应 DOM 元素的可见性监听。

使用示例如下:

// 现代浏览器版本
const imgArr = Array.from(document.querySelectorAll("img"));

const io = new IntersectionObserver((entries) => {
  // entries表示可见性发生变化的DOM元素合集,比如首屏一进来就有三张图片,它们同时由“不可见”变为“可见”
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      // isIntersecting为true,就表示元素可见
      const target = entry.target;
      target.src = target.dataset.src;
      io.unobserve(target);
    }
  });
});

imgArr.forEach((img) => io.observe(img));

具体的使用包括一些细节,如果不记得可以翻阮一峰的教程。IntersectionObserver API 使用教程

古老版本,一种降级方案

古老版本中,用于判断元素是否可见,是通过getClientBoundingRect来做的。getClientBoundingRect 返回的 top 值,是相对于屏幕视窗左上角来定位的。初略地认为: 当这个 top 值 < window.innerHeight 时,就是可见的。

再把这种动态判断逻辑放在滚动事件触发的函数中,为了节省性能,滚动的回调一般用 throttle 节流器包装一层,代码如下:

// 兼容IE及其他古老浏览器版本
const throttle = function (func, wait) {
  let prev = Date.now();

  return function (...args) {
    const now = Date.now();
    const context = this;
    if (now - prev >= wait) {
      func.apply(context, args);
      prev = Date.now();
    }
  };
};

const imgArr = Array.from(document.querySelectorAll("img"));

const lazyLoad = function () {
  const viewportHeight = window.innerHeight;
  imgArr.forEach((img) => {
    const top = img.getBoundingClientRect().top;
    if (top < viewportHeight && !img.src) {
      img.src = img.dataset.src;
    }
  });
};

const tLazyLoad = throttle(lazyLoad, 200);

lazyLoad(); // 这里要先执行一次,因为第一次进来虽然没有滚动,但是也要判断哪些图片可见,并赋上src
document.addEventListener("scroll", tLazyLoad);

总结

综合来看,交叉观察器要准确很多,目前已经被绝大部分现代浏览器支持,如果要做完美兼容,可以先通过"IntersectionObeserver" in window来判断一下window对象有没有这个属性,有就用它,没有就降级处理。