react无限滚动组件的实现示例

 更新时间:2023年05月28日 13:04:48   作者:ymitc  
本文主要介绍了react无限滚动组件的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

上拉无限滚动

核心:判断滚动条是否触底了,触底了就重新加载数据

判断触底:scrollHeight-scrollTop-clientHeight<阈值

容器底部与列表底部的距离(表示还剩多少px到达底部)=列表高度-容器顶部到列表顶部的距离-容器高度

说一下几个概念

scrollHeight:只读属性。表示当前元素的内容总高度,包括由于溢出导致在视图中不可见的内容。这里获取的是列表数据的总高度

scrollTop:可以获取或设置一个元素的内容垂直滚动的像素数。这里获取的是容器顶部到列表顶部的距离,也就是列表卷去的高度

clientHeight:元素content+padding的高度。这里获取的是容器的高度

代码实现:

import * as React from 'react';
import { Component, createElement, ReactNode } from 'react';
interface Props {
  loadMore: Function; // 加载数据的回调函数
  loader: ReactNode; // “加载更多”的组件
  threshold: number; // 到达底部的阈值
  hasMore?: boolean; // 是否还有更多可以加载
  pageStart?: number; // 页面初始页
  initialLoad?: boolean; // 是否第一次就加载
  getScrollParent?: () => HTMLElement; //自定义滚动容器
}
class InfiniteScroll extends Component<Props, any> {
  private scrollComponent: HTMLDivElement | null = null; // 列表数据
  private loadingMore = false; // 是否正在加载更多
  private pageLoaded = 0; // 当前加载页数
  constructor(props: Props) {
    super(props);
    this.scrollListener = this.scrollListener.bind(this); // scrollListener 用到了 this,所以要 bind 一下
  }
  //获取滚动容器
  getParentElement(el: HTMLElement | null): HTMLElement | null {
    const scrollParent =
      this.props.getScrollParent && this.props.getScrollParent();
    if (scrollParent) {
      return scrollParent;
    }
    //默认将当前组件的外层元素作为滚动容器
    return el && el.parentElement;
  }
  // 滚动监听顺
  scrollListener() {
    //列表数据组件
    const node = this.scrollComponent;
    if (!node) return;
    //滚动容器
    const parentNode = this.getParentElement(this.scrollComponent);
    if (!parentNode) return;
    // 核心计算公式
    const offset =
      node.scrollHeight - parentNode.scrollTop - parentNode.clientHeight;
    if (offset < this.props.threshold) {
      this.detachScrollListener(); // 加载的时候去掉监听器
      this.props.loadMore((this.pageLoaded += 1)); // 加载更多
      this.loadingMore = true; // 正在加载更多
    }
  }
  attachScrollListener() {
    const parentElement = this.getParentElement(this.scrollComponent);
    if (!parentElement) return;
    const scrollEl = this.props.useWindow ? window : parentElement;
    scrollEl.addEventListener('scroll', this.scrollListener);
    scrollEl.addEventListener('resize', this.scrollListener);
    //设置滚动条即时不动也会自动触发第一次渲染列表数据
    if (this.props.initialLoad) {
      this.scrollListener();
    }
  }
  detachScrollListener() {
    const parentElement = this.getParentElement(this.scrollComponent);
    if (!parentElement) return;
    parentElement.removeEventListener('scroll', this.scrollListener);
    parentElement.removeEventListener('resize', this.scrollListener);
  }
  componentDidMount() {
    this.attachScrollListener();
  }
  componentDidUpdate() {
    this.attachScrollListener();
  }
  componentWillUnmount() {
    this.detachScrollListener();
  }
  render() {
    const { children, loader } = this.props;
    // 获取滚动元素的核心代码
    return (
      <div ref={(node) => (this.scrollComponent = node)}>
        {children} 很长很长很长的东西
        {loader} “加载更多”
      </div>
    );
  }
}
export default InfiniteScroll;

测试demo

import React, { useEffect, useState } from 'react';
import InfiniteScroll from './InfiniteScroll';
type AsyncFn = () => Promise<void>;
export const delay = (asyncFn: AsyncFn) =>
  new Promise<void>((resolve) => {
    setTimeout(() => {
      asyncFn().then(() => resolve);
    }, 1500);
  });
let counter = 0;
const DivScroller = () => {
  const [items, setItems] = useState<string[]>([]);
  const fetchMore = async () => {
    await delay(async () => {
      const newItems = [];
      for (let i = counter; i < counter + 50; i++) {
        newItems.push(`Counter: ${i} |||| ${new Date().toISOString()}`);
      }
      setItems([...items, ...newItems]);
      counter += 50;
    });
  };
  useEffect(() => {
    fetchMore().then();
  }, []);
  return (
    <div style={{ height: 250, overflow: 'auto', border: '1px solid red' }}>
      <InfiniteScroll
        useWindow={false}
        threshold={50}
        loadMore={fetchMore}
        loader={
          <div className="loader" key={0}>
            Loading ...
          </div>
        }
      >
        {items.map((item) => (
          <div key={item}>{item}</div>
        ))}
      </InfiniteScroll>
    </div>
  );
};
export default DivScroller;

运行结果:

window作容器的无限滚动

window作为滚动组件的话,判断触底的公式不变,获取数据的方法变化了:

offset = 列表数据高度 - 容器顶部到列表顶部的距离 - 容器高度

offset = (当前窗口顶部到列表顶部的距离+offsetHeight) - window.pageOffsetY - window.innerHeight

(当前窗口顶部到列表顶部的距离+offsetHeight)是固定的值,变化的是window.pageOffsetY,也就是说往上拉会window.pageOffsetY变大,offset变小,也就是距离底部越来越近

代码实现

import * as React from 'react';
import { Component, createElement, ReactNode } from 'react';
interface Props {
  loadMore: Function; // 加载数据的回调函数
  loader: ReactNode; // “加载更多”的组件
  threshold: number; // 到达底部的阈值
  hasMore?: boolean; // 是否还有更多可以加载
  pageStart?: number; // 页面初始页
  initialLoad?: boolean; // 是否第一次就加载
  getScrollParent?: () => HTMLElement; //自定义滚动容器
  useWindow?: boolean; // 是否以 window 作为 scrollEl
}
class InfiniteScroll extends Component<Props, any> {
  private scrollComponent: HTMLDivElement | null = null; // 列表数据
  private loadingMore = false; // 是否正在加载更多
  private pageLoaded = 0; // 当前加载页数
  constructor(props: Props) {
    super(props);
    this.scrollListener = this.scrollListener.bind(this); // scrollListener 用到了 this,所以要 bind 一下
  }
  //获取滚动容器
  getParentElement(el: HTMLElement | null): HTMLElement | null {
    const scrollParent =
      this.props.getScrollParent && this.props.getScrollParent();
    if (scrollParent) {
      return scrollParent;
    }
    //默认将当前组件的外层元素作为滚动容器
    return el && el.parentElement;
  }
  // 滚动监听顺
  scrollListener() {
    //列表数据组件
    const node = this.scrollComponent;
    if (!node) return;
    //滚动容器
    const parentNode = this.getParentElement(this.scrollComponent);
    if (!parentNode) return;
    let offset;
    if (this.props.useWindow) {
      const doc =
        document.documentElement ||
        document.body.parentElement ||
        document.body; // 全局滚动容器
      const scrollTop = window.pageYOffset || doc.scrollTop; // 全局的 "scrollTop"
      offset = this.calculateOffset(node, scrollTop);
    } else {
      offset =
        node.scrollHeight - parentNode.scrollTop - parentNode.clientHeight;
    }
    if (offset < this.props.threshold) {
      this.detachScrollListener(); // 加载的时候去掉监听器
      this.props.loadMore((this.pageLoaded += 1)); // 加载更多
      this.loadingMore = true; // 正在加载更多
    }
  }
  calculateOffset(el: HTMLElement | null, scrollTop: number) {
    if (!el) return 0;
    return (
      this.calculateTopPosition(el) +
      el.offsetHeight -
      scrollTop -
      window.innerHeight
    );
  }
  calculateTopPosition(el: HTMLElement | null): number {
    if (!el) return 0;
    return (
      el.offsetTop + this.calculateTopPosition(el.offsetParent as HTMLElement)
    );
  }
  attachScrollListener() {
    const parentElement = this.getParentElement(this.scrollComponent);
    if (!parentElement) return;
    const scrollEl = this.props.useWindow ? window : parentElement;
    scrollEl.addEventListener('scroll', this.scrollListener);
  }
  detachScrollListener() {
    const parentElement = this.getParentElement(this.scrollComponent);
    if (!parentElement) return;
    const scrollEl = this.props.useWindow ? window : parentElement;
    scrollEl.removeEventListener('scroll', this.scrollListener);
  }
  componentDidMount() {
    this.attachScrollListener();
  }
  componentDidUpdate() {
    this.attachScrollListener();
  }
  componentWillUnmount() {
    this.detachScrollListener();
  }
  render() {
    const { children, loader } = this.props;
    // 获取滚动元素的核心代码
    return (
      <div ref={(node) => (this.scrollComponent = node)}>
        {children} 很长很长很长的东西
        {loader} “加载更多”
      </div>
    );
  }
}
export default InfiniteScroll;

测试demo:

import React, { useEffect, useState } from 'react';
import InfiniteScroll from './InfiniteScroll';
type AsyncFn = () => Promise<void>;
export const delay = (asyncFn: AsyncFn) =>
  new Promise<void>((resolve) => {
    setTimeout(() => {
      asyncFn().then(() => resolve);
    }, 1500);
  });
let counter = 0;
const DivScroller = () => {
  const [items, setItems] = useState<string[]>([]);
  const fetchMore = async () => {
    await delay(async () => {
      const newItems = [];
      for (let i = counter; i < counter + 150; i++) {
        newItems.push(`Counter: ${i} |||| ${new Date().toISOString()}`);
      }
      setItems([...items, ...newItems]);
      counter += 150;
    });
  };
  useEffect(() => {
    fetchMore().then();
  }, []);
  return (
    <div style={{ border: '1px solid blue' }}>
      <InfiniteScroll
        useWindow
        threshold={300}
        loadMore={fetchMore}
        loader={
          <div className="loader" key={0}>
            Loading ...
          </div>
        }
      >
        {items.map((item) => (
          <div key={item}>{item}</div>
        ))}
      </InfiniteScroll>
    </div>
  );
};
export default DivScroller;

运行结果:

下滑无限滚动

改变loader的位置

offset计算方法发生改变:offset = scrollTop

考虑一个问题:当下拉加载新数据后滚动条的位置不应该在scrollY = 0 的位置,不然会一直加载新数据

解决办法:

当前 scrollTop = 当前 scrollHeight - 上一次的 scrollHeight + 上一交的 scrollTop parentElement.scrollTop = parentElement.scrollHeight - this.beforeScrollHeight + this.beforeScrollTop

代码实现:

import * as React from 'react';
import { Component, createElement, ReactNode } from 'react';
interface Props {
  loadMore: Function; // 加载数据的回调函数
  loader: ReactNode; // “加载更多”的组件
  threshold: number; // 到达底部的阈值
  hasMore?: boolean; // 是否还有更多可以加载
  pageStart?: number; // 页面初始页
  initialLoad?: boolean; // 是否第一次就加载
  getScrollParent?: () => HTMLElement; //自定义滚动容器
  useWindow?: boolean; // 是否以 window 作为 scrollEl
  isReverse?: boolean; // 是否为相反的无限滚动
}
class InfiniteScroll extends Component<Props, any> {
  private scrollComponent: HTMLDivElement | null = null; // 列表数据
  private loadingMore = false; // 是否正在加载更多
  private pageLoaded = 0; // 当前加载页数
  // isReverse 后专用参数
  private beforeScrollTop = 0; // 上次滚动时 parentNode 的 scrollTop
  private beforeScrollHeight = 0; // 上次滚动时 parentNode 的 scrollHeight
  constructor(props: Props) {
    super(props);
    this.scrollListener = this.scrollListener.bind(this); // scrollListener 用到了 this,所以要 bind 一下
  }
  //获取滚动容器
  getParentElement(el: HTMLElement | null): HTMLElement | null {
    const scrollParent =
      this.props.getScrollParent && this.props.getScrollParent();
    if (scrollParent) {
      return scrollParent;
    }
    //默认将当前组件的外层元素作为滚动容器
    return el && el.parentElement;
  }
  // 滚动监听顺
  scrollListener() {
    //列表数据组件
    const node = this.scrollComponent;
    if (!node) return;
    //滚动容器
    const parentNode = this.getParentElement(this.scrollComponent);
    if (!parentNode) return;
    let offset;
    if (this.props.useWindow) {
      const doc =
        document.documentElement ||
        document.body.parentElement ||
        document.body; // 全局滚动容器
      const scrollTop = window.pageYOffset || doc.scrollTop; // 全局的 "scrollTop"
      offset = this.props.isReverse
        ? scrollTop
        : this.calculateOffset(node, scrollTop);
    } else {
      offset = this.props.isReverse
        ? parentNode.scrollTop
        : node.scrollHeight - parentNode.scrollTop - parentNode.clientHeight;
    }
    // 是否到达阈值,是否可见
    if (
      offset < (this.props.threshold || 300) &&
      node &&
      node.offsetParent !== null
    ) {
      this.detachScrollListener();
      this.beforeScrollHeight = parentNode.scrollHeight;
      this.beforeScrollTop = parentNode.scrollTop;
      if (this.props.loadMore) {
        this.props.loadMore((this.pageLoaded += 1));
        this.loadingMore = true;
      }
    }
  }
  calculateOffset(el: HTMLElement | null, scrollTop: number) {
    if (!el) return 0;
    return (
      this.calculateTopPosition(el) +
      el.offsetHeight -
      scrollTop -
      window.innerHeight
    );
  }
  calculateTopPosition(el: HTMLElement | null): number {
    if (!el) return 0;
    return (
      el.offsetTop + this.calculateTopPosition(el.offsetParent as HTMLElement)
    );
  }
  attachScrollListener() {
    const parentElement = this.getParentElement(this.scrollComponent);
    if (!parentElement) return;
    const scrollEl = this.props.useWindow ? window : parentElement;
    scrollEl.addEventListener('scroll', this.scrollListener);
  }
  detachScrollListener() {
    const parentElement = this.getParentElement(this.scrollComponent);
    if (!parentElement) return;
    const scrollEl = this.props.useWindow ? window : parentElement;
    scrollEl.removeEventListener('scroll', this.scrollListener);
  }
  componentDidMount() {
    this.attachScrollListener();
  }
  componentDidUpdate() {
    if (this.props.isReverse && this.props.loadMore) {
      const parentElement = this.getParentElement(this.scrollComponent);
      if (parentElement) {
        // 更新滚动条的位置
        parentElement.scrollTop =
          parentElement.scrollHeight -
          this.beforeScrollHeight +
          this.beforeScrollTop;
        this.loadingMore = false;
      }
    }
    this.attachScrollListener();
  }
  componentWillUnmount() {
    this.detachScrollListener();
  }
  render() {
    const { children, loader, isReverse } = this.props;
    const childrenArray = [children];
    if (loader) {
      // 根据 isReverse 改变 loader 的插入方式
      isReverse ? childrenArray.unshift(loader) : childrenArray.push(loader);
    }
    return (
      <div ref={(node) => (this.scrollComponent = node)}>{childrenArray}</div>
    );
  }
}
export default InfiniteScroll;

测试demo:

import React, { useEffect, useState } from 'react';
import InfiniteScroll from './InfiniteScroll';
type AsyncFn = () => Promise<void>;
export const delay = (asyncFn: AsyncFn) =>
  new Promise<void>((resolve) => {
    setTimeout(() => {
      asyncFn().then(() => resolve);
    }, 1500);
  });
let counter = 0;
const DivReverseScroller = () => {
  const [items, setItems] = useState<string[]>([]);
  const fetchMore = async () => {
    await delay(async () => {
      const newItems = [];
      for (let i = counter; i < counter + 50; i++) {
        newItems.push(`Counter: ${i} |||| ${new Date().toISOString()}`);
      }
      setItems([...items, ...newItems]);
      counter += 50;
    });
  };
  useEffect(() => {
    fetchMore().then();
  }, []);
  return (
    <div style={{ height: 250, overflow: 'auto', border: '1px solid red' }}>
      <InfiniteScroll
        isReverse
        useWindow={false}
        threshold={50}
        loadMore={fetchMore}
        loader={
          <div className="loader" key={0}>
            Loading ...
          </div>
        }
      >
        {items
          .slice()
          .reverse()
          .map((item) => (
            <div key={item}>{item}</div>
          ))}
      </InfiniteScroll>
    </div>
  );
};
export default DivReverseScroller;

运行结果

优化

1、在mousewheel里通过e.preventDefault解决"加载更多"时间超长的问题

2、添加被动监听器,提高页面滚动性能

3、优化render函数

最终优化版源码

总结

无限滚动原理的核心就是维护当前的offset值

1、向下无限滚动:offset = node.scrollHeight - parentNode.scrollTop - parentNode.clientHeight

2、向上无限滚动:offset = parentNode.scrollTop

3、window为滚动容器向下无限滚动:offset = calculateTopPosition(node) + node.offsetHeight - window.pageYoffset - window.innerHeight

其中calculateTopPosition函数通过递归计算当前窗口顶部距离浏览器窗口顶部的距离

4、window为滚动容器向上无限滚动:offset = window.pageYoffset || doc.scrollTop

其中doc = document.documentElement || document.body.parentElement || document.body

到此这篇关于react无限滚动组件的实现示例的文章就介绍到这了,更多相关react无限滚动内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 解决react-connect中使用forwardRef遇到的问题

    解决react-connect中使用forwardRef遇到的问题

    这篇文章主要介绍了解决react-connect中使用forwardRef遇到的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-05-05
  • React组件中的this的具体使用

    React组件中的this的具体使用

    这篇文章主要介绍了React组件中的this的具体使用,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-02-02
  • React、Vue中key的作用详解 (key的内部原理解析)

    React、Vue中key的作用详解 (key的内部原理解析)

    key是虚拟DOM对象的标识,当状态中的数据发生变化时,Vue会根据[新数据]生成[新的虚拟DOM],本文给大家介绍React、Vue中key的作用详解 (key的内部原理解析),感兴趣的朋友一起看看吧
    2023-10-10
  • React Hooks之useDeferredValue钩子用法示例详解

    React Hooks之useDeferredValue钩子用法示例详解

    useDeferredValue钩子的主要目的是在React的并发模式中提供更流畅的用户体验,特别是在有高优先级和低优先级更新的情况下,本文主要讲解一些常见的使用场景及其示例
    2023-09-09
  • React中使用react-file-viewer问题

    React中使用react-file-viewer问题

    这篇文章主要介绍了React中使用react-file-viewer问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-09-09
  • 详解React Native与IOS端之间的交互

    详解React Native与IOS端之间的交互

    React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,是Facebook早先开源的JS框架 React 在原生移动应用平台的衍生产物,支持iOS和安卓两大平台。本文将介绍React Native与IOS端之间的交互。
    2021-06-06
  • React通过redux-persist持久化数据存储的方法示例

    React通过redux-persist持久化数据存储的方法示例

    这篇文章主要介绍了React通过redux-persist持久化数据存储的方法示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-02-02
  • 详解如何在React中有效地监听键盘事件

    详解如何在React中有效地监听键盘事件

    React是一种流行的JavaScript库,用于构建用户界面,它提供了一种简单而灵活的方式来创建交互式的Web应用程序,在React中,我们经常需要监听用户的键盘事件,以便根据用户的输入做出相应的反应,本文将向您介绍如何在React中有效地监听键盘事件,并展示一些常见的应用场景
    2023-11-11
  • 详解React-Native解决键盘遮挡问题(Keyboard遮挡问题)

    详解React-Native解决键盘遮挡问题(Keyboard遮挡问题)

    本篇文章主要介绍了React-Native解决键盘遮挡问题(Keyboard遮挡问题),具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-07-07
  • 详解react组件通讯方式(多种)

    详解react组件通讯方式(多种)

    这篇文章主要介绍了详解react组件通讯方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-05-05

最新评论