不到20行js实现高性能图片懒加载

什么是懒加载相信大家都知道,一句话概括,就是当需要出现的时候再加载。

当页面上有很多图片(即:有很多img标签)的时候,浏览器会依次执行get请求拿到每个图片的二进制流,这实际上是在消耗用户的流量和服务器带宽。而不管是pc设备还是移动端设备,屏幕大小总是有限的,无法一次性看到所有图片,那么看不到的图片就没必要加载,等滚动屏幕,img元素进入浏览器可视区域时再加载。

基本原理

那如何做到这一点呢?我们知道浏览器的<img>标签,是当src属性有值时,才会去加载对应的图片地址,那么我们是不是可以实时的判断img元素有没有进入可视区域。

基于这个事实,我们就可以给每张图片一个默认占位图:

<img src="占位图.png">

826900e91570bdb14dd5d635df7abe99.png再设置一个自定义属性,值为真实的图片地址:

<img src="占位图.png" data-src="https://k.sinaimg.cn/n/news/transform/20171113/puY7-fynship2141885.jpg/w700d1q75cms.jpg?by=cms_fixed_width">

然后在「合适的时机」data-src的值赋给src,让真实的图片显示,这就是图片「懒加载」的基本原理。

那什么时候是“合适的时机”呢,接着往下看。

先看看老旧的方案

js监听页面滚动

window.addEventListener("scroll", () => {})

img标签的顶部到可视区域顶部的距离,小于可视区域高度时,我们就认为图片进入了可视区域,画张图表示:

红色框表示浏览器可视区域,蓝色框表示图片元素d2ee568e0cd11cf37395f6a5057eb93e.png就得到类似如下代码:

window.addEventListener("scroll", () => {
  const img = document.querySelectorAll('img')
  img.forEach(img => {
    const rect = img.getBoundingClientRect();
    console.log("rect", rect);
    if (rect.top < document.body.clientHeight) {
      img.src = img.dataset.src
    }
  })
})

Element.getBoundingClientRect() 方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。d789a510d4e4903129f97261c9c6f09c.png884fade51cbb72f0911da680397e1dd6.png这样虽然也能实现,但可以看到,这样需要一直在监听页面滚动,而且触发的非常频繁,非常耗费性能,为了解决这一点,还需要在另外加防抖函数。并且还要计算滚动位置,很麻烦。

新方案

Intersection Observer API9aecd6bfce614a55a25dd6d326e622ce.png这个api是浏览器原生自带的,用于异步检测目标元素与父级或顶级元素是否产生交叉。

两个重点:

  • 异步

  • 检测是否交叉

异步意味着提高性能,检测交叉意味着我们不需要再去计算滚动位置,它会直接告诉我们是不是需要加载了。

这个api的核心实现是提供了一个IntersectionObserver构造函数,这个构造函数接收两个参数:

  • 检测到交叉时的回调函数

  • 一个用于自定义配置什么情况下才会产生交叉的对象

用代码表示更好理解

let observer = new IntersectionObserver(
  () => {
    console.log('交叉了')
  },
  {
    root: null,
    rootMargin: "0px 0px 0px 0px",
    threshold: 0.5
  }
);
// 获取所有的图片元素
const imgs = document.querySelectorAll("img");
// 遍历这些元素
imgs.forEach((img) => {
  // 观察这些元素
  observer.observe(img);
});

先看第二个参数,是一个对象,有三个属性,这三个属性可以都不传,但必须传个对象,可以是空对象。

  • root: 表示要观察的目标元素(我们这里是img),与哪个父元素产生交叉,不传或者传null,就表示根元素,即浏览器的视口。

  • rootMargin: 表示root元素的偏移量,怎么理解呢,正常是目标元素与root元素本身所在的位置交叉时触发回调函数,rootMargin可以改变与root元素交叉时的位置,这里必须用张图来表示了。72185daf84cc67487cdcb2c49ad46323.png红色区域表示root元素,蓝色表示图片,如果rootMargin传了"10px 10px 10px 10px",当图片与往外偏移10px的root元素产生交叉时就会触发回调。这个参数还可以传负数,如果传了"-10px -10px -10px -10px",就是往内偏移。这个用到的应该不多吧!

  • threshold:是一个0~1之间的值,表示一个触发的阈值,如果是0,只要目标元素一碰到root元素,就会触发,如果是1,表示目标元素完全进入root元素范围,才会触发。

new IntersectionObserver的实例observer有个observe方法,用于观察目标元素,因为我们要观察的图片有很多,所以需要遍历。

再看第一个参数,是一个回调函数

let observer = new IntersectionObserver(
  (entries) => {
    console.log('交叉了')
    console.log(entries)
  },
  {
    root: null,
    rootMargin: "0px 0px 0px 0px",
    threshold: 0.5
  }
);
// 获取所有的图片元素
const imgs = document.querySelectorAll("img");
// 遍历这些元素
imgs.forEach((img) => {
  // 用observe方法观察这些元素
  observer.observe(img);
});

e2d5dbf585700107f90874056a5931fd.png这个回调函数提供了一个entries参数,是一个数组,observer实例观察了多少个元素,这个数组就有多少项,每一项是个对象,对象中有一个isIntersecting属性,当前元素与root元素交叉时为true,否则就为false。

let observer = new IntersectionObserver(
  (entries) => {
    console.log("交叉了");
    console.log(entries);
    for (const entrie of entries) {
       if (entrie.isIntersecting) {
          const img = entrie.target;
          img.src = img.dataset.src;
          observer.unobserve(img);
       }
    }
  },
  {
    root: null,
    rootMargin: "0px 0px 0px 0px",
    threshold: 0.5
  }
);
// 获取所有的图片元素
const imgs = document.querySelectorAll("img");
// 遍历这些元素
imgs.forEach((img) => {
  // 用observe方法观察这些元素
  observer.observe(img);
});

当产生交叉时,把imgdata-src赋值给src,就让真实的图片显示了,然后再通过observer实例提供的unobserve方法,取消对该元素的观察。

到这里我们的图片懒加载就完成了,这样做的好处是:

  • 高性能,当目标元素与root元素交叉时才会触发一次,然后就不会再触发了。

  • 用起来简单,不用计算滚动距离。

看效果

acbc325cdb8f6b71df05ccd2495dd7eb.gif

浏览器兼容性

09601b3b925e7e4d27174f7c3e9dc695.png可以看到,我们用到的IntersectionObserver构造函数,以及几个必须用的属性和方法,都是完全支持的。完全不必担心低版本浏览器。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值