俩种方式实现懒加载,深入浅出IntersectionObserver

1,957 阅读5分钟

懒加载是一种前端常见优化性能技术,其核心思想是将页面中的资源延迟加载,即按需加载。这种方式有助于减少初始页面加载时间,减轻服务器和带宽的压力,从而加速页面加载速度

举个🌰 比方说当我们开发一个有很多图片的长网页时,一般先加载出现在视口内的几张图片,当滚动条滚动到相应图片的位置时再去加载别的图片,这种延迟加载的方式称之为 ~

大抵有俩种方式实现懒加载,一个是监听onscroll属性,另外一个是文章的重点,IntersectionObserver实现

监听onscroll属性实现

用前人的一张图来解释一下 Img

  • clientHeight:浏览器视口的高度
  • scrollTop:滚动轴滚动的距离
  • offsetTop:图片的头部距离浏览器顶部的高度(注意不是距离视口顶部的高度)
const imgs = document.querySelectorAll('img');
const lazyLoad = () =>{
  let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  let winTop = window.innerHeight; 
  for(let i=0 ;i < imgs.length; i++){
    // 此时加载图片
    if(imgs[i].offsetTop < scrollTop + winTop ){
      imgs[i].src = imgs[i].getAttribute('data-src');
    } 
  }
}
window.onscroll = lazyLoad

2023-11-01 04.39.50.gif 可以看到确实是已经实现了懒加载,但注意到控制台的打印,lazyLoad函数频繁触发,这个时候可以用节流throttle来优化一下

const throttle = (fn, delay) => {
    let currentTime = Date.now()
    return (...args) => {
      nowTime = Date.now()
      if (nowTime - currentTime > delay) {
        fn(...args)
        currentTime = Date.now()
      }
    }
  }

Img

感觉效果还行。。但是这种方法是有缺点的。由于scroll事件密集发生,计算量很大 在这样的背景下,IntersectionObserver登场

IntersectionObserver 操作

MDN的解释是

IntersectionObserver 接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉状态的方法。其祖先元素或视口被称为根(root)

Img

  • 图1 target元素刚与root元素交叉
  • 图2 target元素与root元素交叉中
  • 图3 target元素即将离开root元素,仍交叉

目标元素与root元素刚开始交叉和目标元素与root元素刚开始不交叉都能检测到

IntersectionObserver的执行时机

  • IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发

  • IntersectionObserver采用的是requestIdleCallback (请求空闲回调),即只有线程空闲下来才会执行观察器。也就是说这个观察器的优先级非常低,只在其他任务执行完浏览器有了空闲才会执行

IntersectionObserver API

  1. 创建 IntersectionObserver 实例

    const observer = new IntersectionObserver(callback, options);
    
  2. 定义回调函数:回调函数会在目标元素与视口发生交叉时执行,entries是一个包含有关观察的目标元素的交叉信息的数组, observerIntersectionObserver 的实例

    function callback(entries, observer) {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          // 目标元素进入视口
        } else {
          // 目标元素离开视口
        }
      });
    }
    
  3. 观察目标元素

    const target = document.querySelector('.目标元素');
    observer.observe(target);
    
  4. 配置options:包含三个可选参数,root:dom根元素、rootMargin:根元素的外边距,threshold:阈值 详细可看MDN

    const options = {
      root: document.querySelector('.根元素'),
      threshold: 0.5, // 交叉的阈值,0.5表示目标元素一半进入视口时触发回调,
      rootMargin: "10px 20px 30px 40px" // (top、right、bottom、left)
    };
    
  5. 处理回调函数:根据 entry.isIntersecting 属性的值确定目标元素是进入还是离开视口

  6. 停止观察:当不需要观察某个目标元素时unobserve 方法来停止观察

    observer.unobserve(target)
    
  7. 断开观察器:当不需要使用 IntersectionObserver 时用 disconnect 方法断开

    observer.disconnect()
    

注册的回调函数将会在主线程中被执行,所以该函数执行速度要尽可能的快,如果有一些耗时的操作需要执行还是使用Window.requestIdleCallback() 方法

IntersectionObserverEntry 对象

在浏览器随便找个dom元素打印一下entries Img

具体含义

  • boundingClientRect:目标元素的矩形区域的信息
  • intersectionRatio:表示目标元素的多少部分可见于视口,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0
  • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
  • isIntersecting:目标元素与根元素是否相交
  • isVisible:Intersection Observer v2 引入的“可见性”的概念,若isVisible为true,即目标元素完全不被其他内容遮挡,并且没有应用会改变或扭曲其在屏幕上的显示的视觉效果,若为false则不能保证可见性(一般不用)
  • rootBounds:根元素的矩形区域的信息, getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
  • target:被观察的目标元素,是一个 DOM 节点对象
  • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒

实现懒加载

const imgs = document.querySelectorAll('[data-src]')
const observer = new IntersectionObserver(entries => {
	entries.forEach(item => {
		if (item.isIntersecting) {
			item.target.src = item.target.dataset.src 
			observer.unobserve(item.target) // 停止观察当前元素 避免不可见时候再次调用callback函数
      console.log(item.target.dataset.src  +'图片已加载')
		}
	})
})
imgs.forEach(item => {
	observer.observe(item)
})
// observer.disconnect() // 离开页面或者不需要observer时

Img 看起来效果不错!

提问: intersectionRatio是否可以判断相交🤔

如果你滚动速度很快,当浏览器检测到相交时已经越过了 0 那个临界值,存在了实际的相交面积,entry.intersectionRatio > 0, 此时等效于isIntersecting为true。但如果滚动页面速度很慢,当目标元素的顶部和视口底部刚好挨上时浏览器检测到相交,回调函数触发,但此时 entry.intersectionRatio 等于 0 ,就不等效于isIntersecting为true的时机了,继续向下滚回调函数也不会触发

详细可看代码片段

参考