Vue使用交叉观察器实现图片懒加载和滚动加载更多

371 阅读5分钟

什么是交叉观察器(IntersectionObserver)

IntersectionObserver 是一个浏览器 API,它可以用来检测一个元素是否进入了视口(即屏幕可见区域),或者两个元素之间的交集情况。接下来我们来看看如何使用它以及它的各个参数和配置分别代表什么意思。

1. 创建 IntersectionObserver 实例

const observer = new IntersectionObserver(callback, options);

callback:这是一个函数,当被观察的目标元素与根容器的交集发生变化时,这个函数会被调用。它接收两个参数:entries,observer

  • entries:一个 IntersectionObserverEntry 对象的数组,表示每个被观察的目标元素的交集状态。entries中的对象当中的属性包括以下这几种。
属性类型描述
timenumber交集变化发生的时间戳,以毫秒为单位。
targetElement被观察的目标元素。
isIntersectingboolean表示目标元素是否与根容器有交集。如果为 true,则表示目标元素进入了视口。
intersectionRationumber目标元素与根容器的交集比例,范围是 0 到 1。
boundingClientRectDOMRect目标元素的边界矩形,相对于视口的坐标。
rootBoundsDOMRect根容器的边界矩形,相对于视口的坐标。
intersectionRectDOMRect目标元素与根容器的交集区域的矩形,相对于视口的坐标。

其中值得我们关注的就是isIntersectingtarget

  • observer:当前的 IntersectionObserver 实例,可以在回调函数中用于停止观察某个元素或断开观察器。

option:是其对应的配置,是一个对象的形式,里面包含三个属性。

参数类型描述默认值
rootElement 或 null指定一个元素作为根容器,默认为null,意味着使用顶层文档的视窗(视口)。null
rootMarginstring一个字符串,指定了根容器边缘的外边距,类似于CSS中的margin属性,但它接受的单位是px或%,并且可以设置四个不同的值。"0px"
thresholdnumber表示目标元素与根容器相交的比例。当相交比例达到任何一个阈值时,就会触发回调函数。可以是一个数字或数字数组。数据范围是0(观察元素刚刚进入根容器)~1(观察元素完全进入根容器)0

2. 来看一个简单的使用示例吧

// 1.创建一个 IntersectionObserver 实例
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log('视口发生交叉了');
    }
  });
}, { threshold: 0.1 }); // 当目标元素有10%进入视口时触发,root不写表示默认值为null,即视口

// 获取要观察的目标元素(例如,页面中的一个 div)
const targetElement = document.querySelector('.target');

// 开始观察目标元素
observer.observe(targetElement);

图片懒加载

图片懒加载(Lazy Loading)是一种优化网页性能的技术,它允许图片在用户滚动到它们所在的区域时才加载,而不是在页面加载时立即加载所有图片。下面我来介绍一下几种简单的实现图片懒加载的方式。

方法优点缺点适用场景
img 标签自带 loading="lazy"简单易用,浏览器原生支持旧版浏览器不支持现代浏览器环境,快速实现懒加载
IntersectionObserver灵活性高,性能优化好需要编写 JavaScript 代码,旧版浏览器需 polyfill需要自定义懒加载逻辑的场景
Element Plus 的 el-image功能丰富,易于集成依赖 Element Plus 库使用 Element Plus 的项目,需要更多功能的场景

接下来我将着重介绍使用交叉观察器实现图片懒加载

交叉观察器实现图片懒加载

我们来理一下使用交叉观察器实现图片懒加载的思路

  1. 准备很多图片,首先为每个图片设置一个默认的占位图(如灰色方块,或者是默认图片)就是将所有图片都用默认路径代替,然后将真实路径存放在 :data-src="image" 中。
     <img 
        :src="defaultImage" 
        :data-src="image"
        class="lazy-image"
      />
  1. 使用浏览器提供的 IntersectionObserver API 来检测图片是否进入视口(即用户当前可见的屏幕区域)。当图片即将或已经进入视口时(取决于你设置的阈值范围0-1),API 会触发回调函数,回调函数中我们将图片的真实路径赋值在src上。
  2. 一旦图片开始加载,就不再需要继续观察它,因此可以在加载完成后调用 observer.unobserve(img) 来停止对该图片的观察,优化性能。

下方是示例代码:

<template>
  <div class="container">
    <div 
      v-for="(image, index) in images" 
      :key="index"
      class="image-wrapper"
    >
      <img 
        :src="defaultImage" 
        :data-src="image"
        class="lazy-image"
      />
    </div>
  </div>
</template>

<script setup>
import { onMounted, ref } from 'vue'

// 默认占位图(一个灰色背景的base64图片)
const defaultImage = 'data:image/svg+xml;base64,PHN2ZyB0PSIxNjkwNzg3MjE2NTI3IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik04NTMuMzMzMzMzIDg1My4zMzMzMzNIMTcwLjY2NjY2N2MtNDYuOTMzMzMzIDAtODUuMzMzMzMzLTM4LjQtODUuMzMzMzM0LTg1LjMzMzMzM1YyNTZjMC00Ni45MzMzMzMgMzguNC04NS4zMzMzMzMgODUuMzMzMzM0LTg1LjMzMzMzM2g2ODIuNjY2NjY2YzQ2LjkzMzMzMyAwIDg1LjMzMzMzMyAzOC40IDg1LjMzMzMzNCA4NS4zMzMzMzN2NTEyYzAgNDYuOTMzMzMzLTM4LjQgODUuMzMzMzMzLTg1LjMzMzMzNCA4NS4zMzMzMzN6IiBmaWxsPSIjRUJFQkVCIi8+PC9zdmc+'

// 生成30张测试图片
const images = ref([])
for (let i = 1; i <= 30; i++) {
    //这里使用的是第三方图片生成库,由于是网络获取所以会有一些延迟
  images.value.push(`https://picsum.photos/400/300?random=${i}`)
}

onMounted(() => {
  const lazyImages = document.querySelectorAll('.lazy-image')
  
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target
        // 获取真实图片地址
        const realSrc = img.dataset.src
        // 设置真实图片
        img.src = realSrc
        // 图片加载完成后添加已加载类名
        img.onload = () => {
          img.classList.add('loaded')
        }
        // 停止观察该图片
        observer.unobserve(img)
      }
    })
  }, {
    // 图片出现 10% 就触发加载
    threshold: 0.1
  })

  // 观察所有图片
  lazyImages.forEach(img => observer.observe(img))
})
</script>

<style scoped>
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 20px;
}

.image-wrapper {
  aspect-ratio: 4/3;
  background-color: #f5f5f5;
  border-radius: 8px;
  overflow: hidden;
}

.lazy-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: opacity 0.3s ease, transform 0.3s ease;
  opacity: 0.5;
  transform: scale(1.02);
}

.lazy-image.loaded {
  opacity: 1;
  transform: scale(1);
}
</style>

具体效果如下:(控制台-->网络-->图片),可以看到随着我鼠标的滚动,随着我不断地滚动,只有滚动到我视口的图片才开始进行加载。为啥图片显示会有延迟呢,原因是我的图片是通过第三方网站获取的 picsum.photos/400/300路径上的参数是图片尺寸。

20241222221646_rec_.gif

接下来我们来看看,没有添加图片懒加载的效果,我们刷新页面之后,图片全部都加载出来了

20241222223910_rec_.gif

交叉观察器实现滚动加载更多

具体思路就当加载器进入视口之后就加载更多的数据,如果有不懂滚动加载更多原理的伙伴可以看看我上一篇文章juejin.cn/post/744782…,这里我就不多说了,直接上代码了,代码里面有注释

具体效果如下:

20241222232350_rec_.gif

最后附上图片懒加载与滚动加载更多的源码

<template>
  <div class="container">
    <div
      v-for="(image, index) in images.slice(0, pageSize)"
      :key="index"
      class="image-wrapper"
    >
      <img :src="defaultImage" :data-src="image" class="lazy-image" />
    </div>
    <div>
      <T-loading :loading="loading" text="正在加载中" class="loading">
      </T-loading>
    </div>
  </div>
</template>

<script setup>
import { onMounted, ref } from "vue";

// 默认占位图(一个灰色背景的base64图片)
const defaultImage =
  "data:image/svg+xml;base64,PHN2ZyB0PSIxNjkwNzg3MjE2NTI3IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik04NTMuMzMzMzMzIDg1My4zMzMzMzNIMTcwLjY2NjY2N2MtNDYuOTMzMzMzIDAtODUuMzMzMzMzLTM4LjQtODUuMzMzMzM0LTg1LjMzMzMzM1YyNTZjMC00Ni45MzMzMzMgMzguNC04NS4zMzMzMzMgODUuMzMzMzM0LTg1LjMzMzMzM2g2ODIuNjY2NjY2YzQ2LjkzMzMzMyAwIDg1LjMzMzMzMyAzOC40IDg1LjMzMzMzNCA4NS4zMzMzMzN2NTEyYzAgNDYuOTMzMzMzLTM4LjQgODUuMzMzMzMzLTg1LjMzMzMzNCA4NS4zMzMzMzN6IiBmaWxsPSIjRUJFQkVCIi8+PC9zdmc+";

// 生成30张测试图片
const images = ref([]);
for (let i = 1; i <= 30; i++) {
  images.value.push(`https://picsum.photos/400/300?random=${i}`);
}
const loading = ref(true);
const pageSize = ref(9);
onMounted(() => {
  //用于图片懒加载
  const lazyImages = document.querySelectorAll(".lazy-image");
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const img = entry.target;
          // 获取真实图片地址
          const realSrc = img.dataset.src;
          // 设置真实图片
          img.src = realSrc;
          // 图片加载完成后添加已加载类名
          img.onload = () => {
            img.classList.add("loaded");
          };
          // 停止观察该图片
          observer.unobserve(img);
        }
      });
    },
    {
      // 图片出现 10% 就触发加载
      threshold: 0.1,
    }
  );

  // 观察所有图片
  lazyImages.forEach((img) => {
    observer.observe(img);
  });

  //观察加载器,用于滚动加载更多
  const loadingElement = document.querySelector(".loading");
  const observerLoading = new IntersectionObserver(
    async (entries) => {
      //因为只有一个元素所以我们可以简单点
      if (entries[0].isIntersecting) {
        const loadTarget = entries[0].target;
        await new Promise((resolve) => {
          setTimeout(() => {
            resolve("模拟请求时间");
          }, 300);
        });
        pageSize.value = pageSize.value + 6;
        if (pageSize.value >= 30) {
          pageSize.value = 30;
          loading.value = false;
          // 停止观察
          observer.unobserve(loadTarget);
        }
      }
    },
    {
      // 加载完全包括在视口就触发
      threshold: 1,
    }
  );
  observerLoading.observe(loadingElement);
});
</script>

<style scoped>
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 20px;
}

.image-wrapper {
  aspect-ratio: 4/3;
  background-color: #f5f5f5;
  border-radius: 8px;
  overflow: hidden;
}

.lazy-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: opacity 0.3s ease, transform 0.3s ease;
  opacity: 0.5;
  transform: scale(1.02);
}

.lazy-image.loaded {
  opacity: 1;
  transform: scale(1);
}
</style>