实战案例:前端滚动加载的两种实现方式

背景

在实际项目中,经常遇到需要滚动加载数据的场景,比如消息列表、图片列表等。为了方便后续遇到相似场景可以很快的找到实现的逻辑代码,本文整理了日常工作中我常用的两种方法。

技术背景

本文示例基于 React + Tailwind 技术栈。

目录

  • 原生 onScroll 事件实现滚动加载
  • 原生 IntersectionObserver API 实现滚动加载

原生 onScroll 事件实现滚动加载

描述

onScroll 事件既可以监听 window 也可以监听某个元素。用它实现滚动加载的核心是基于滚动元素的 scrollTop (元素滚动的距离)scrollHeight (滚动元素的高度) 以及 clientHeight (元素可视区域的高度) 配合起来的差值计算作为判断。

  • 判断滚动元素是否处于顶部位置,判断其 scollTop === 0 即可,也可以根据实际情况判断稍微大一点的差值,比如 scollTop <= 你定义的值
  • 判断滚动元素是否处于底部位置,判断其属性 scrollHeight - scrollTop - clientHeight === 0 ,也可以根据实际情况判断稍微大一点的差值,比如 scrollHeight - scrollTop - clientHeight <= 你定义的值

示例代码

javascript 复制代码
import React, { useRef, useEffect, useState } from 'react';
import _ from 'loadsh'

const ScrollMore = () => {
    const [data, setData] = useState({ count: 0 })
    const [msg, setMsg] = useState('暂无操作')

    const scrollHandle = _.throttle((e) => {
        const current = e.target
        if (current.scrollHeight - current.scrollTop - current.clientHeight < 1) {
            // 滚动到底部,做你想做的其他事
            setData({ count: data.count + 1 })
            setMsg('到底了,数据 + 1')
        } else if (current.scrollTop < 1) {
            // 滚动到顶部,做你想做的其他事
            setData({ count: data.count - 1 })
            setMsg('到顶了,数据 - 1')
        } else {
            setMsg('暂无操作')
        }
    }, 100)

    return (
        <div className='mt-[20px] ml-[100px]'>
            <h1 className='text-[18px] font-[700]'>onScroll 事件实现滚动加载</h1>
            <div>数据变化:<span className='ml-[10px]'>{data.count}</span></div>
            <div className='mt-[20px]'>{msg}</div>
            <div className='mt-[20px] w-[200px] h-[680px] overflow-auto bg-[#ccc]'
                onScroll={scrollHandle}>
                <div className='h-[1200px]' ></div>
            </div>
        </div >
    );
};

export default ScrollMore;

tips:实际项目中可以使用类似 loadsh 的工具库,对 onScroll 事件处理函数做一层节流处理,降低其触发的频率。

效果演示

原生 IntersectionObserver API 实现滚动加载

描述

IntersectionObserver API 是一种用于监听页面元素可见性变化的浏览器 API,它提供了一种通过观察目标元素与其祖先或视窗(viewport)之间的交叉区域来实现延迟加载、懒加载,或者自动执行其他操作的方式。详情可以参考 MDN说明

用它实现滚动加载的原理就是在滚动容器中,放置一个不影响页面渲染的元素。实际项目中可以将其透明度设置为 0,大小设置为 1px * 1px,然后监听其是否处于视窗可视区域,如果是则认为滚动元素处于底部或顶部位置。

示例代码

javascript 复制代码
import React, { useRef, useEffect, useState } from 'react';


const Scroll = () => {
    const topTriggerRef = useRef(null); // 用于判断是否到顶部的占位元素
    const bottomTriggerRef = useRef(null); // 用于判断是否到底部的占位元素

    const data = useRef({ count: 0 })
    const [msg, setMsg] = useState('暂无操作')


    // 在这里声明observer变量
    useEffect(() => {
        // 创建 IntersectionObserver 实例
        const observer = new IntersectionObserver((entries) => {
            entries.forEach((entry) => {
                if (entry.isIntersecting) {
                    if (entry.target === bottomTriggerRef.current) {
                        // 滚动到底部,做你想做的其他事
                        data.current.count += 1
                        setMsg('到底了,数据 + 1')
                    } else if (entry.target === topTriggerRef.current) {
                        // 滚动到底部,做你想做的其他事
                        data.current.count -= 1
                        setMsg('到顶了,数据 - 1')
                    }

                } else {
                    setMsg('暂无操作')
                }
            });
        }, {
            root: null, // 视口作为参照物
            rootMargin: '0px',
            threshold: 1.0, // 触发时机,1.0 表示目标元素完全在视口内
        });

        // 如果 bottomTriggerRef 当前指向的元素存在,则开始观察
        if (bottomTriggerRef.current) {
            observer.observe(bottomTriggerRef.current);
        }
        if (topTriggerRef.current) {
            observer.observe(topTriggerRef.current);
        }
        // 需要返回的清理函数
        return () => observer.disconnect();
    }, []); // 空依赖数组确保仅在组件挂载时执行

    return (
        <div className='mt-[20px] ml-[100px]'>
            <h1 className='text-[18px] font-[700]'>IntersectionObserver API实现滚动加载</h1>
            <div>数据变化:<span className='ml-[10px]'>{data.current.count}</span></div>
            <div className='mt-[20px]'>{msg}</div>
            <div className='mt-[20px] w-[200px] h-[680px] overflow-auto bg-[#ccc]'>
                <div ref={topTriggerRef} className='text-center '>用于检测顶部的元素</div>
                <div className='h-[1200px]' ></div>
                <div ref={bottomTriggerRef} className='text-center '>用于底部检测的元素</div>
            </div>
        </div >
    );
};

export default Scroll;

效果演示

注意事项

如果你仔细观察上面的代码,会发现跟 onScroll 的案例有细微的差别,就是对数据 data 我没有使用 useState,而使用的是 useRef,如果你感兴趣的话,可以去尝试一下,看看会发生什么。

这里我直接说结论,在 IntersectionObserver 的回调方法中,使用 useState,无法按预期的达到我们改变 state 的效果。

本文还是以案例为主,因此不过多的分析原理,这里贴出来一个 Stackoverflow 中有人遇到的相似问题,里面有相关原理回答,感兴趣的同学可以深入研究一下。问题地址:usestate-and-intersectionobserver-test-on-react

同理,我认为这不是个例,在我之前的文章 实战案例:ChatGPT 打字机效果的三种实现方式 里,在函数组件中使用 @microsoft/fetch-event-source 库时也遇到了类似的问题。

因此建议大家在实际项目中遇到无法按预期改变 state 时,可以尝试以下两种解决方案:

  • 使用 useRef 来定义你的变量。
  • 将函数组件改为类组件,通过 this.setState(obj,callback) 的回调函数 callback 来实现 state 改变之后你的预期逻辑。

杂谈

  • 上述案例只是说明了最基础的使用方式,如果你想将其变为可以复用的 hook 或工具类,可以考虑让 AI 助手帮你完成,然后做简单调试即可。
  • 上述的两个方案各有利弊,个人认为:
    • onScroll 方案相对稳定,计算距离比较准确一些,但是需要在使用时做好类似节流的优化,避免多次触发 onScroll 的事件处理函数,从而引发性能问题。
    • IntersectionObserver 对性能这块相对比较友好,但是它过于灵敏,因此如何控制好元素显隐逻辑处理会是一个比较大的考验。
  • React 中关于 setState 更新的问题,在项目开发中很难遇到,但当我们遇到了,可以尝试改变策略,使用最小案例原则,尽可能复现问题,区分是 React 框架的问题还是我们逻辑代码的问题。
相关推荐
HEX9CF16 分钟前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
凌云行者28 分钟前
使用rust写一个Web服务器——单线程版本
服务器·前端·rust
华农第一蒟蒻44 分钟前
Java中JWT(JSON Web Token)的运用
java·前端·spring boot·json·token
积水成江1 小时前
关于Generator,async 和 await的介绍
前端·javascript·vue.js
Z3r4y1 小时前
【Web】portswigger 服务端原型污染 labs 全解
javascript·web安全·nodejs·原型链污染·wp·portswigger
___Dream1 小时前
【黑马软件测试三】web功能测试、抓包
前端·功能测试
金灰1 小时前
CSS3练习--电商web
前端·css·css3
人生の三重奏1 小时前
前端——js补充
开发语言·前端·javascript
Tandy12356_1 小时前
js逆向——webpack实战案例(一)
前端·javascript·安全·webpack
TonyH20021 小时前
webpack 4 的 30 个步骤构建 react 开发环境
前端·css·react.js·webpack·postcss·打包