懒加载(滚动加载)

懒加载(滚动加载)

  • 在我们平常的开发中,我们会加载一些列表数据,这些数据的加载会影响到页面的加载速度,导致用户的等待时间变长,影响用户的体验
  • 所以很多数据量比较大的列表数据都会有翻页的功能,但是翻页会让用户多一些操作,影响用户的体验
  • 所以有一种懒加载的方式就是滚动加载,当用户滚动到列表的底部时,再加载下一页的数据,这样可以避免一次性加载所有数据,导致页面的加载速度变慢,同时也减少了用户的操作,提升了用户的交互体验
  • 平时我常用的滚动加载主要是传统 scroll 监听,但是最近深入研究了一下这个,发现还有其他的方法来实现滚动加载

基本原理

无论用哪种方法,逻辑都差不多:

  1. 监听滚动事件scrollIntersectionObserver
  2. 判断是否接近底部
  3. 触发加载函数
  4. 请求数据并渲染
  5. 直到数据加载完成

两种常见实现方式

方式一:传统 scroll 监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<div id="list" style="height: 400px; overflow-y: auto;">
<!-- 内容 -->
</div>

<script>
const container = document.getElementById('list');

container.addEventListener('scroll', () => {
// 滚动条总高度
const scrollHeight = container.scrollHeight;
// 当前滚动位置
const scrollTop = container.scrollTop;
// 可视区域高度
const clientHeight = container.clientHeight;

// 当滚动到底部时
if (scrollTop + clientHeight >= scrollHeight - 10) {
console.log('到底了,加载更多数据...');
loadMore();
}
});

function loadMore() {
// 模拟加载
setTimeout(() => {
const item = document.createElement('div');
item.innerText = '新数据';
container.appendChild(item);
}, 500);
}
</script>
  • 这种方式我们平时的使用频率还是挺高的,我就不做过多的介绍了

方式二:现代 IntersectionObserver

IntersectionObserver 可以观察某个元素是否进入视口,非常适合滚动加载的**“底部探针”**写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<div id="list" style="height: 400px; overflow-y: auto;">
<div>数据 1</div>
<div>数据 2</div>
<!-- ... -->
<div id="sentinel">加载中...</div>
</div>

<script>
const sentinel = document.getElementById('sentinel');

const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
console.log('触发加载更多...');
loadMore();
}
}, { root: document.getElementById('list') });

observer.observe(sentinel);

function loadMore() {
setTimeout(() => {
const container = document.getElementById('list');
for (let i = 0; i < 5; i++) {
const div = document.createElement('div');
div.innerText = '新数据 ' + Date.now();
container.insertBefore(div, sentinel);
}
}, 500);
}
</script>
  • 这种方式我以前是不了解的,但是最近在看一些文章的时候,发现这个方式还是比较常用的,所以我就来学习一下

  • 首先我们先来了解一下 IntersectionObserver

    • IntersectionObserver 能异步地监听一个或多个目标元素(target)与某个容器(root,默认是浏览器视口)之间的交叉情况,比监听 scroll 更高效(浏览器内部优化、不会频繁回调),代码也更清晰。

    • 核心 API

      1
      2
      3
      4
      const observer = new IntersectionObserver(callback, options);
      observer.observe(targetElement);
      observer.unobserve(targetElement);
      observer.disconnect(); // 停止观察所有目标

      callback(entries, observer):当被观察元素与 root 的交叉状态发生变化时被调用。entries 是数组,每个 entryIntersectionObserverEntry,包含:

      • entry.isIntersecting(布尔)——是否与 root 有交叉(大多数场景直接用它)
      • entry.intersectionRatio(0~1)——可见比例
      • entry.targetentry.boundingClientRectentry.intersectionRectentry.rootBoundsentry.time

      options

      • root:容器元素(默认 null = 浏览器视口)。如果你用滚动容器(overflow:auto),把它设为那个元素
      • rootMargin:四边距(类似 CSS margin),可以用来提前触发(例如 '0px 0px 200px 0px' 表示在元素距离底部 200px 时就触发)
      • threshold:触发阈值,单个数字或数组。例如 0(一像素可见就触发),0.5(可见 50% 时触发),也可以是 [0, 0.5, 1]
      1
      2
      3
      4
      5
      6
      const observer = new IntersectionObserver(entries => {
      if (entries[0].isIntersecting) {
      console.log('触发加载更多...');
      loadMore();
      }
      }, { root: document.getElementById('list') });
      • 这个地方很多人第一次用 IntersectionObserver 都会疑惑,为啥大多数例子里都是 entries[0]

        1. entries 到底是什么

          • 这是浏览器在调用你写的 callback 时传进来的第一个参数

          • 它是一个 数组(准确地说是 IntersectionObserverEntry[]

          • 这个数组里包含了 当前这个观察器里,所有交叉状态发生变化的元素 对应的 entry 对象

          • 如果你只用 observe() 观察 一个元素,那么一次回调里 entries 数组里 通常只有一个元素,所以大家就直接写 entries[0]

          • 如果你用一个 observer 观察了 多个元素,那么一次回调可能有多个元素变化(比如同时两个元素进入视口),这时 entries 里就会有多个 entry

        2. 为什么一般直接用 entries[0]

          • 因为大部分场景(比如无限滚动)只观察一个“探针元素”,数组里就只有一个 entry,直接取第一个就够了,写起来方便,但这并不是强制规则,只是“偷懒”的写法

在Vue项目中使用VueUse来快速实现滚动加载

  • VueUse 中可以使用 useInfiniteScroll 实现滚动加载

    • 用来实现无限滚动加载,封装了 scroll 事件监听和触发条件判断

    • 原理是:监听容器的滚动位置,当接近底部时触发你定义的回调

  • 官方文档:https://vueuse.org/core/useInfiniteScroll/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div ref="container" style="height: 400px; overflow-y: auto; border: 1px solid;">
<div v-for="item in items" :key="item">{{ item }}</div>
<div v-if="loading">加载中...</div>
</div>
</template>

<script setup>
import { ref } from 'vue'
import { useInfiniteScroll } from '@vueuse/core'

const container = ref(null)
const items = ref(Array.from({ length: 20 }, (_, i) => i + 1))
const loading = ref(false)

useInfiniteScroll(
container, // 监听的滚动容器
async () => {
loading.value = true
await new Promise(r => setTimeout(r, 1000)) // 模拟请求
items.value.push(...Array.from({ length: 10 }, (_, i) => items.value.length + i + 1))
loading.value = false
},
{ distance: 50 } // 距底部 50px 触发
)
</script>