无限滚动

原生JS实现无限滚动

无限滚动又称触底加载,它大致意思是滚到到底部时获得新数据,再进行加载展示。
在前端开发的过程中,这个问题100%的开发人员都会碰到,所以就讲一下我是如何处理的。


方法一:监听DOM元素并实时计算

1. 基础原理

从原生JS的层面上来讲,基本原理如下:

  1. 获取带有滚动条div的DOM元素:用于限制可视范围,并生成滚动条获取其中滚动条距离顶端距离,以及获取可视范围高度,还有获取内容总高度,再监听滚动事件
  2. 触发滚动时计算:滚动条距离顶端距离+可视范围高度>=内容总高度-提前触发距离

2. 代码实现

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<template>
<div class="scroll_container" id="scrollableDiv">
<div class="scroll_display" v-for="i in index" :key="i">
<h2>我是第{{ i }}个展示内容!!!</h2>
</div>
</div>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue'
const index = ref(20)
//滚动到底触发动作
const ScrollLoad = () => {
index.value += 20
}
onMounted(() => {
// 1.获取带有滚动条`div`的BOM
const ScrollContainer = document.getElementById('scrollableDiv')
// 2.监听滚动事件
ScrollContainer?.addEventListener('scroll', () => {
const scrollTop = ScrollContainer.scrollTop // 获取div的滚动位置
const divHeight = ScrollContainer.clientHeight // 获取div的高度
const contentHeight = ScrollContainer.scrollHeight // 获取div内容的高度
//3.计算是否到达底部
if (scrollTop + divHeight >= contentHeight) {
//新增数据
ScrollLoad()
}
})
})
</script>

<style scoped>
.scroll_container {
width: 100%;
height: 600px;
background-color: yellowgreen;
overflow: auto;
}
.scroll_display {
background-color: rgb(255, 255, 255);
height: 300px;
margin: 10px;
}
</style>

3. 完善与思考

因为使用Vue进行项目编写比较方便,它可以快速实现功能以及路由跳转,所以我就用它写代码了,不过写完后其实还有几个问题:

  1. 一直快速往下滚动会频繁的触发操作,应该做节流处理
  2. 在代码中的响应式数据其实是通过Vue实现的,有空时我将使用原生JS写一个,这也会设计到Vue响应式的本质。
  3. 我为什么会在onMounted中进行BOM绑定监听,这是因为若写在setup()中时,DOM元素还不存在,此时则无法绑定上。而通过onMounted(组件加载后),进行绑定就能绑定上。
  4. 其实一直在监听并计算是会消耗许多内存,所以应该用IntersectionObserver这个原生的API实现即可,用户不会关注到代码实现,他们只会在乎实际体验。

方法二:通过IntersectionObserver(交叉观察器)接口

1. 基础原理

交叉观察器的原理:

  1. 创建观察器实例:创建一个IntersectionObserver实例,并指定一个回调函数
  2. 选择目标元素选择要观察的目标元素
  3. 开始观察:通过调用IntersectionObserver实例的observe方法,开始观察目标元素。浏览器会异步监视目标元素其祖先元素视窗之间的交叉状态,直至监视停止
  4. 触发回调函数:当目标元素的交叉状态发生变化时,会触发指定的回调函数,并传入一个包含相关信息的数组(entries)。每个entry对象都包含了目标元素交叉状态信息,如isIntersecting(表示元素是否当前可见)、intersectionRatio(表示目标元素与视口或指定祖先元素的交叉比例)等。

2. 代码实现

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<template>
<div class="scroll_container">
<div class="scroll_display" v-for="i in observerIndex" :key="i">
<h2>我是交叉观察器,第{{ i }}个展示内容!!!</h2>
</div>
<div
id="observerDiv"
style="background-color: aliceblue; margin: 6px; height: 100px"
>
我是底部div,用于观察以及展示正在加载...
</div>
</div>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue'
const index = ref(20)
//滚动到底触发动作
const ScrollLoad = () => {
index.value += 20
}
onMounted(() => {
// 1.获取带有滚动条`div`的BOM
const observerContainer = document.querySelector('#observerDiv')
// 2.定义配置对象
const options = {
root: null, //观察视口,它得是目标元素的祖先元素!!!
rootMargin: '0px',//扩充观察视口范围:上 右 下 左
threshold: 0.99,//目标元素触发时可见度,数组时则每到达一个时就触发一次
}
//3.创建交叉观察器
//虽然observerLoad没有接收参数,但可以填入entries, observer来接收参数
const observer = new IntersectionObserver(observerLoad, options)
//4.监视对象
if (observerContainer) {
observer.observe(observerContainer)
} else {
console.error('观察目标不存在')
}
})
</script>

<style scoped>
.scroll_container {
width: 100%;
height: 600px;
background-color: yellowgreen;
overflow: auto;
}
.scroll_display {
background-color: rgb(255, 255, 255);
height: 300px;
margin: 10px;
}
</style>

3. 完善与思考

通过交叉观察器的方法来设置无限滚动是最好的方法,即可以设置加载显示状态,也减少了性能消耗
想法扩展

  1. 用它实现懒加载的效果应该挺不错的。
  2. 可以确认图片是否展示在了视口上。
  3. 记录目标元素的展示范围,并执行对于的方法,例如动画显示效果改变加载定时器,等等方法。
    问题
  • 我想如下场景 能不能实现:
    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
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    <!DOCTYPE html>
    <html lang="en">

    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Intersection Observer with Sticky Root Element</title>
    <style>
    .scroll_container {
    height: 300px;
    overflow-y: scroll;
    border: 1px solid black;
    }

    #root {
    position: sticky;
    height: 100px;
    top: 50px;
    background-color: rgba(173, 216, 230, 0.623);
    padding: 10px;
    border: 2px solid blue;
    }

    #test_box {
    height: 600px;
    background-color: lightgreen;
    }

    #target {
    height: 200px;
    background-color: lightcoral;
    }
    </style>
    </head>

    <body>

    <div class="scroll_container" id="scroll_container">
    <div id="root">
    <h2>我是视口</h2>
    </div>
    <div id="test_box">
    <h2>我是填充盒子</h2>
    </div>
    <div id="target">
    <h2>我是目标盒子</h2>
    </div>
    <div id="test_box">
    <h2>我是填充盒子</h2>
    </div>
    </div>

    <script>
    const rootElement = document.getElementById('root');
    const targetElement = document.getElementById('target');

    const observerOptions = {
    root: rootElement, // 将 root 绑定为粘性定位的视口元素
    rootMargin: '0px',
    threshold: 0.1 // 目标盒子可见 10% 时触发
    };

    const observerCallback = (entries) => {
    entries.forEach(entry => {
    if (entry.isIntersecting) {
    console.log('目标盒子与 root 相交了');
    } else {
    console.log('目标盒子与 root 分离了');
    }
    });
    };

    const observer = new IntersectionObserver(observerCallback, observerOptions);
    observer.observe(targetElement); // 开始观察目标盒子
    </script>

    </body>

    </html>
  • 结果不能实现,看来相交并不是人眼所看见的相交,而是目标元素包含视口元素中,才能使用,这样子的结果就只是加载后打印目标盒子与 root 分离了
  • 如果想实现大概是观察目标换成scroll_container,而observerOptions中的rootMargin得变成'负的scroll_container上边距离root顶端的px值 0px 负的scroll_container下边距离root底端的px值 0px',这样子来模拟看到的视口相交,。
  • 顺便吐槽一下,B站上课程中讲无限滚动实现通常还是方法1,而不是方法2。我看了MND上交叉观察器不是实验性,按理来说应该是用这个好一点,不过也可能是方法1更接近底层原理吧。