最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

react無限滾動組件的實現(xiàn)示例

 更新時間:2023年05月28日 13:04:48   作者:ymitc  
本文主要介紹了react無限滾動組件的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

上拉無限滾動

核心:判斷滾動條是否觸底了,觸底了就重新加載數(shù)據(jù)

判斷觸底:scrollHeight-scrollTop-clientHeight<閾值

容器底部與列表底部的距離(表示還剩多少px到達底部)=列表高度-容器頂部到列表頂部的距離-容器高度

說一下幾個概念

scrollHeight:只讀屬性。表示當前元素的內容總高度,包括由于溢出導致在視圖中不可見的內容。這里獲取的是列表數(shù)據(jù)的總高度

scrollTop:可以獲取或設置一個元素的內容垂直滾動的像素數(shù)。這里獲取的是容器頂部到列表頂部的距離,也就是列表卷去的高度

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

代碼實現(xiàn):

import * as React from 'react';
import { Component, createElement, ReactNode } from 'react';
interface Props {
  loadMore: Function; // 加載數(shù)據(jù)的回調函數(shù)
  loader: ReactNode; // “加載更多”的組件
  threshold: number; // 到達底部的閾值
  hasMore?: boolean; // 是否還有更多可以加載
  pageStart?: number; // 頁面初始頁
  initialLoad?: boolean; // 是否第一次就加載
  getScrollParent?: () => HTMLElement; //自定義滾動容器
}
class InfiniteScroll extends Component<Props, any> {
  private scrollComponent: HTMLDivElement | null = null; // 列表數(shù)據(jù)
  private loadingMore = false; // 是否正在加載更多
  private pageLoaded = 0; // 當前加載頁數(shù)
  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;
  }
  // 滾動監(jiān)聽順
  scrollListener() {
    //列表數(shù)據(jù)組件
    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(); // 加載的時候去掉監(jiān)聽器
      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);
    //設置滾動條即時不動也會自動觸發(fā)第一次渲染列表數(shù)據(jù)
    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作為滾動組件的話,判斷觸底的公式不變,獲取數(shù)據(jù)的方法變化了:

offset = 列表數(shù)據(jù)高度 - 容器頂部到列表頂部的距離 - 容器高度

offset = (當前窗口頂部到列表頂部的距離+offsetHeight) - window.pageOffsetY - window.innerHeight

(當前窗口頂部到列表頂部的距離+offsetHeight)是固定的值,變化的是window.pageOffsetY,也就是說往上拉會window.pageOffsetY變大,offset變小,也就是距離底部越來越近

代碼實現(xiàn)

import * as React from 'react';
import { Component, createElement, ReactNode } from 'react';
interface Props {
  loadMore: Function; // 加載數(shù)據(jù)的回調函數(shù)
  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; // 列表數(shù)據(jù)
  private loadingMore = false; // 是否正在加載更多
  private pageLoaded = 0; // 當前加載頁數(shù)
  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;
  }
  // 滾動監(jiān)聽順
  scrollListener() {
    //列表數(shù)據(jù)組件
    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(); // 加載的時候去掉監(jiān)聽器
      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計算方法發(fā)生改變:offset = scrollTop

考慮一個問題:當下拉加載新數(shù)據(jù)后滾動條的位置不應該在scrollY = 0 的位置,不然會一直加載新數(shù)據(jù)

解決辦法:

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

代碼實現(xiàn):

import * as React from 'react';
import { Component, createElement, ReactNode } from 'react';
interface Props {
  loadMore: Function; // 加載數(shù)據(jù)的回調函數(shù)
  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; // 列表數(shù)據(jù)
  private loadingMore = false; // 是否正在加載更多
  private pageLoaded = 0; // 當前加載頁數(shù)
  // isReverse 后專用參數(shù)
  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;
  }
  // 滾動監(jiān)聽順
  scrollListener() {
    //列表數(shù)據(jù)組件
    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) {
      // 根據(jù) 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;

運行結果

優(yōu)化

1、在mousewheel里通過e.preventDefault解決"加載更多"時間超長的問題

2、添加被動監(jiān)聽器,提高頁面滾動性能

3、優(yōu)化render函數(shù)

最終優(yōu)化版源碼

總結

無限滾動原理的核心就是維護當前的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函數(shù)通過遞歸計算當前窗口頂部距離瀏覽器窗口頂部的距離

4、window為滾動容器向上無限滾動:offset = window.pageYoffset || doc.scrollTop

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

到此這篇關于react無限滾動組件的實現(xiàn)示例的文章就介紹到這了,更多相關react無限滾動內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • 解決React報錯Property?'value'?does?not?exist?on?type?EventTarget

    解決React報錯Property?'value'?does?not?exist?on?

    這篇文章主要為大家介紹了React報錯Property?'value'?does?not?exist?on?type?EventTarget的解決方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-12-12
  • React API集成與路由的最佳實踐

    React API集成與路由的最佳實踐

    本文給大家介紹了React API集成與路由的最佳實踐,本文結合實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧
    2026-02-02
  • react?Scheduler?實現(xiàn)示例教程

    react?Scheduler?實現(xiàn)示例教程

    這篇文章主要為大家介紹了react?Scheduler?實現(xiàn)示例教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-09-09
  • React + Node.js實現(xiàn)圖片上傳功能

    React + Node.js實現(xiàn)圖片上傳功能

    最近筆者在開發(fā)個人博客的后臺管理系統(tǒng),里面用到了圖片上傳相關的功能,在這里記錄并分享一下,希望可以幫到大家,話不多說直接開始吧,感興趣的朋友可以參考下
    2024-01-01
  • 記一次react前端項目打包優(yōu)化的方法

    記一次react前端項目打包優(yōu)化的方法

    這篇文章主要介紹了記一次react前端項目打包優(yōu)化的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-03-03
  • react實現(xiàn)Radio組件的示例代碼

    react實現(xiàn)Radio組件的示例代碼

    這篇文章主要介紹了react實現(xiàn)Radio組件的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2021-04-04
  • React Native 混合開發(fā)多入口加載方式詳解

    React Native 混合開發(fā)多入口加載方式詳解

    這篇文章主要介紹了React Native 混合開發(fā)多入口加載方式詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-09-09
  • React生命周期原理與用法踩坑筆記

    React生命周期原理與用法踩坑筆記

    這篇文章主要介紹了React生命周期原理與用法,結合實例形式總結分析了react生命周期原理、用法及相關注意事項,需要的朋友可以參考下
    2020-04-04
  • react-redux的connect示例詳解

    react-redux的connect示例詳解

    connect()是react-redux中的核心方法之一,它將react組件預redux中的Store真正連接在一起,這篇文章主要介紹了react-redux的connect詳解,需要的朋友可以參考下
    2023-01-01
  • React中合成事件的實現(xiàn)

    React中合成事件的實現(xiàn)

    React合成事件是對瀏覽器原生事件的封裝,提供跨瀏覽器一致性API,采用事件委托機制提升性能,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2025-09-09

最新評論

松阳县| 泽普县| 博罗县| 绥江县| 姜堰市| 哈密市| 碌曲县| 龙山县| 黄山市| 深水埗区| 涿州市| 潼南县| 上高县| 三都| 景泰县| 平陆县| 万载县| 芒康县| 建湖县| 曲靖市| 大安市| 桂东县| 连山| 温州市| 田东县| 呼图壁县| 惠东县| 曲松县| 昔阳县| 和林格尔县| 岳西县| 五常市| 新余市| 醴陵市| 丹棱县| 共和县| 永福县| 修文县| 新兴县| 炎陵县| 奉化市|