Vue3 + IntersectionObserver 实现滚动加载数据

0 阅读3分钟

传统滚动加载数据实现方式

在传统的前端中实现无线滚动加载,我们肯定会条件反射的想到scroll事件,通过监听scroll 事件,在事件回调中通过计算scrollHeight,scrollY,innerHeight,然后得出请求下一页的条件。这种实现方式虽然也能实现滚动加载数据的效果。但是有几个缺点:

  • 需要我们自己 计算scrollHeight,scrollY,innerHeight 得出请求下一页的时机
  • 由于scroll 事件触发非常频繁,会有一些性能问题,可能还需要我们自己封装防抖节流操作
  • 很容易出现bug,触发下一页的条件处理不好时很可能会出现一直请求,导致性能消耗较大

IntersectionObserver

今天给大家分享的是使用IntersectionObserver 来实现滚动加载数据。IntersectionObserver 是一个交叉口观察器。IntersectionObserver提供了一种异步观察目标元素与其祖先元素顶级文档视口交叉部分变化的方法。

IntersectionObserver 是一个构造器,接收两个参数,第一个参数是一个回调函数,当目标元素可见百分比超过阈值时调用的函数。回调函数接收两个参数:

  • entries 一个IntersectionObserverEntry对象的数组,每个被触发的阈值
  • observer 被调用的IntersectionObserver实例。

第二个参数是一个options , options 有三个参数,具体含义可查看mdn: developer.mozilla.org/en-US/docs/…

image.png

使用Vue3 + IntersectionObserver 实现滚动加载数据

相对于传统的实现方式,使用IntersectionObserver 实现具有以下3个优点:

  1. 性能更好:不依赖主线程,不会造成性能问题
  2. 使用简单:无需手动计算元素位置
  3. 精确度高:可以精确控制触发时机

1. 先创建一个Vue3 项目

pnpm create vite load-more --template vue

cd load-more

pnpm i

运行项目 pnpm run dev

注意: 最新版的vite 需要node版本大于22,否则会报错

2. 项目结构设计如下:

image.png

3. App.vue

<template>
  <div>
    <PhotoList></PhotoList>
  </div>
</template>

<script setup>
import PhotoList from "./components/PhotoList.vue";
</script>

<style scoped></style>

3. PhotoList.vue

<template>
  <div id="listId" class="list">
    <PhotoItem v-for="item in list" :key="item.id" :item="item" />
    <Loading id="loadingId" v-if="!isNoMore" />
    <NoMore v-else />
  </div>
</template>

<script setup>
import { ref, onMounted, watch } from "vue";
import PhotoItem from "./PhotoItem.vue";
import Loading from "./Loading.vue";
import NoMore from "./NoMore.vue";

const page = ref(1);
const list = ref([]);
const isNoMore = ref(false);
watch(
  page,
  async () => {
    const result = await fetch(
      `http://jsonplaceholder.typicode.com/posts?_page=${page.value}`
    );
    const data = await result.json();
    if (!data.length) {
      isNoMore.value = true;
    }
    list.value = list.value.concat(data);
  },
  {
    immediate: true,
  }
);

onMounted(() => {
  const ob = new IntersectionObserver(
    (entries) => {
      console.log(entries);
      if (entries[0].isIntersecting) {
        console.log("00", list.value.length);
        if (list.value.length) {
          page.value++;
        }
      }
    },
    {
      root: document.getElementById("listId"),
      threshold: 0.5,
    }
  );

  ob.observe(document.getElementById("loadingId"));
});
</script>

<style scoped>
.list {
  height: 100vh;
  overflow: auto;
}
</style>

PhotoList 是实现滚动加载的核心,主要实现思路如下:

  • 在watch中监听page的变化,当page 变化时请求数据,将请求到的数据拼接到list数据上
  • 设置watch 的immediate 属性为true,因为我们第一次请进页面的时候也需要请求数据
  • 在onMounted 中创建IntersectionObserver
  • IntersectionObserver 观察了listId根元素和loadingId目标元素
  • 当出现交叉状态时(entries[0].isIntersecting 判断是否为交叉状态)且list 数据长度大于0就更新page。watch 监听page变化就会继续请求数据了。

4. PhotoItem.vue

<template>
  <div class="item">
    <h2>{{ item.title }}</h2>
    <p>{{ item.body }}</p>
  </div>
</template>

<script setup>
const props = defineProps({
  item: {
    type: Object,
    default() {
      return {};
    },
  },
});
</script>

<style scoped>
.item {
  border-bottom: 1px solid #ddd;
  padding-bottom: 16px;
}
.item h2 {
  white-space: nowrap; /* 禁止换行 */
  overflow: hidden; /* 隐藏溢出内容 */
  text-overflow: ellipsis; /* 显示省略号 */
}
.item p {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
}
</style>

5. Loading.vue

<template>
  <div class="loading-wrap">
    <div class="dot-spinner">
      <div class="dot"></div>
      <div class="dot"></div>
      <div class="dot"></div>
    </div>
  </div>
</template>

<script setup></script>

<style scoped>
.loading-wrap {
  padding: 20px 0;
}
.dot-spinner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.dot {
  width: 12px;
  height: 12px;
  background-color: #09f;
  border-radius: 50%;
  animation: bounce 1.4s infinite ease-in-out;
}

.dot:nth-child(1) {
  animation-delay: -0.32s;
}

.dot:nth-child(2) {
  animation-delay: -0.16s;
}

@keyframes bounce {
  0%,
  80%,
  100% {
    transform: scale(0);
  }
  40% {
    transform: scale(1);
  }
}
</style>


6. NoMore.vue

<template>
  <div class="no-more">
    <p>没有更多了</p>
  </div>
</template>

<script setup></script>

<style scoped>
.no-more {
  display: flex;
  justify-content: center;
  align-items: center;
}
.no-more::before {
  content: "";
  display: block;
  width: 100%;
  border-bottom: 1px solid #ddd;
}
</style>


演示效果:

load-more.gif

总结

今天主要分享了传统实现滚动加载数据的缺点和使用IntersectionObserver 实现滚动加载的便利性。

感谢收看,今天就分享到这里了,本篇已收录到Vue 知识储备,欢迎关注