IntersectionObserver实现图片懒加载

所有废墟上的一切,也都将于结束之后重新开始。 —— 镜·神寂

1. 前言

作为在掘金摸🐟已经很久的一名前端er,今天终于迈出了写文章的步伐,主要用于记录平时工作中或者学习中遇到的一些问题以及笔记,方便以后查阅,当然也希望能够帮助更多可爱的前端er。其实从很早以前就有了这个想法,但是每一次都没有坚持下去,现在从头开始,希望为时未晚。

这篇文章的起源来自于最近的一个产品需求,相爱相杀的产品经理希望在移动端预览转码后的PDF、PPT等文件,一般而言转码后的PDF、PPT等都会生成大量的图片,这时就涉及到了图片懒加载的实现。在原有的印象中,图片懒加载的实现都是通过监听scroll事件实现:

  1. 添加自定义属性:给img标签添加data-src,值为图片的url,同时不要设置src属性
  2. 判断目标元素与视口的交叉状态:通过获取元素的getBoundingClientRect属性的top值和页面的clientHeight进行对比,如果top值小于clientHeight,则说明元素出现在可视区域之内
  3. 设置真实的src:当元素出现在可视区域内时,将真实的图片地址赋值给目标元素的src属性

但是,今天在调研的过程中发现了IntersectionObserver这个API完全贴合了我的需求(PS:是我太落后了🙈~)。下面开始进入今天的正题吧!

2. IntersectionObserver 简介

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

下面这些情况都需要用到相交检测:

  • 图片懒加载——当图片滚动到可见时才进行加载
  • 内容无限滚动——也就是用户滚动到接近内容底部时直接加载更多,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉
  • 检测广告的曝光情况——为了计算广告收益,需要知道广告元素的曝光情况
  • 在用户看见某个区域时执行任务或播放动画

具体的属性以及方法就不赘述了,请直接参考MDN文档

3. 基础用法

// 回调函数
const callback =(entries, observer) => {
  entries.forEach(entry => {
    // entry.boundingClientRect
    // entry.intersectionRatio
    // entry.intersectionRect
    // entry.isIntersecting
    // entry.rootBounds
    // entry.target
    // entry.time
  });
};

// 参数配置
const options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
};

// 1. 实例化一个IntersectionObserver对象,并传入相应参数和回调函数
const observer = new IntersectionObserver(callback, options);

// 2. 指定一个目标元素进行观察
const target = document.querySelector('#listItem')
observer.observe(target);

// 3.销毁时,一定要取消观察对象,否则会一直监听
observer.unobserve(target);

以上提到的entry下每个属性的具体函数,请查阅MDN

4. 图片懒加载的实现

由于项目中我们统一的技术栈为Vue3 + Tsx,所以下面采用tsx的写法来编写。

import { defineComponent, ref, toRefs, PropType, onMounted, onUnmounted } from 'vue';
import styles from './index.module.scss';
import defaultImage from '@/assets/images/defaultImage.png';

export default defineComponent({
  props: {
    // 图片集合
    imagesList: {
      type: Array as PropType<string[]>,
      required: true,
    },
  },
  setup(props) {
    const { imagesList } = toRefs(props);
    const observer = ref<any>(null);
    const root = ref<HTMLElement | null>(null);

    // 监视器的回调函数
    const handlerObserve = (entries: any) => {
      entries.forEach(({ isIntersecting, target }: any) => {
        if (isIntersecting) {
          const targetImg = target.children[0];
          targetImg.src = targetImg.dataset.src;
          // 修改过src属性之后,即可移除data-src属性并且取消监视
          targetImg.removeAttribute('data-src');
          observer.value.unobserve(target);
        }
      });
    };

    // 针对图片容器添加监听器
    const addObserve = () => {
      const list = document.querySelectorAll('.image-item-observe') as NodeListOf<Element>;
      list.forEach((item: Element) => {
        observer.value.observe(item);
      });
    };

    // 初始化监视器
    const initObserve = () => {
      observer.value = new IntersectionObserver(handlerObserve, {
        root: root.value,
        rootMargin: '0px 0px 500px 0px', // 监视区向下拓展500px
      });
      addObserve();
    };

    onMounted(async () => {
      initObserve();
    });

    onUnmounted(() => {
      // 关闭观察器
      observer.value.disconnect();
    });

    return () => (
      <div ref={root} class={styles['image-list-container']}>
        {
          imagesList.value.map((item: string, index: number) => (
            <div class={`${styles['image-item-wrapper']} image-item-observe`}>
              {/* 设置一个默认的缺省图,避免在加载过程中出现白屏的现象 */}
              <img
                class={styles['image-item']}
                src={defaultImage}
                data-src={item}
                key={index}
                width="100%"
              />
            </div>
          ))
        }
      </div>
    );
  },
});

具体效果如下图所示(200张图的加载,整体看起来还是👌的):

QQ20221003-111618-HD.gif

5. 结语

第一次写文章,算是迈出了第一步,希望以后能够坚持下去。就我自身而言,写文章主要是为了记录在工作中遇到的问题,以便后期的回顾和查阅。不足之处,还请大家包涵,以后请多多指教。

6. 参考资料