react無限滾動(dòng)組件的實(shí)現(xiàn)示例
上拉無限滾動(dòng)
核心:判斷滾動(dòng)條是否觸底了,觸底了就重新加載數(shù)據(jù)
判斷觸底:scrollHeight-scrollTop-clientHeight<閾值
容器底部與列表底部的距離(表示還剩多少px到達(dá)底部)=列表高度-容器頂部到列表頂部的距離-容器高度
說一下幾個(gè)概念
scrollHeight:只讀屬性。表示當(dāng)前元素的內(nèi)容總高度,包括由于溢出導(dǎo)致在視圖中不可見的內(nèi)容。這里獲取的是列表數(shù)據(jù)的總高度
scrollTop:可以獲取或設(shè)置一個(gè)元素的內(nèi)容垂直滾動(dòng)的像素?cái)?shù)。這里獲取的是容器頂部到列表頂部的距離,也就是列表卷去的高度
clientHeight:元素content+padding的高度。這里獲取的是容器的高度
代碼實(shí)現(xiàn):
import * as React from 'react'; import { Component, createElement, ReactNode } from 'react'; interface Props { loadMore: Function; // 加載數(shù)據(jù)的回調(diào)函數(shù) loader: ReactNode; // “加載更多”的組件 threshold: number; // 到達(dá)底部的閾值 hasMore?: boolean; // 是否還有更多可以加載 pageStart?: number; // 頁面初始頁 initialLoad?: boolean; // 是否第一次就加載 getScrollParent?: () => HTMLElement; //自定義滾動(dòng)容器 } class InfiniteScroll extends Component<Props, any> { private scrollComponent: HTMLDivElement | null = null; // 列表數(shù)據(jù) private loadingMore = false; // 是否正在加載更多 private pageLoaded = 0; // 當(dāng)前加載頁數(shù) constructor(props: Props) { super(props); this.scrollListener = this.scrollListener.bind(this); // scrollListener 用到了 this,所以要 bind 一下 } //獲取滾動(dòng)容器 getParentElement(el: HTMLElement | null): HTMLElement | null { const scrollParent = this.props.getScrollParent && this.props.getScrollParent(); if (scrollParent) { return scrollParent; } //默認(rèn)將當(dāng)前組件的外層元素作為滾動(dòng)容器 return el && el.parentElement; } // 滾動(dòng)監(jiān)聽順 scrollListener() { //列表數(shù)據(jù)組件 const node = this.scrollComponent; if (!node) return; //滾動(dòng)容器 const parentNode = this.getParentElement(this.scrollComponent); if (!parentNode) return; // 核心計(jì)算公式 const offset = node.scrollHeight - parentNode.scrollTop - parentNode.clientHeight; if (offset < this.props.threshold) { this.detachScrollListener(); // 加載的時(shí)候去掉監(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); //設(shè)置滾動(dòng)條即時(shí)不動(dòng)也會(huì)自動(dòng)觸發(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; // 獲取滾動(dòng)元素的核心代碼 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;
運(yùn)行結(jié)果:
window作容器的無限滾動(dòng)
window作為滾動(dòng)組件的話,判斷觸底的公式不變,獲取數(shù)據(jù)的方法變化了:
offset = 列表數(shù)據(jù)高度 - 容器頂部到列表頂部的距離 - 容器高度
offset = (當(dāng)前窗口頂部到列表頂部的距離+offsetHeight) - window.pageOffsetY - window.innerHeight
(當(dāng)前窗口頂部到列表頂部的距離+offsetHeight)是固定的值,變化的是window.pageOffsetY,也就是說往上拉會(huì)window.pageOffsetY變大,offset變小,也就是距離底部越來越近
代碼實(shí)現(xiàn)
import * as React from 'react'; import { Component, createElement, ReactNode } from 'react'; interface Props { loadMore: Function; // 加載數(shù)據(jù)的回調(diào)函數(shù) loader: ReactNode; // “加載更多”的組件 threshold: number; // 到達(dá)底部的閾值 hasMore?: boolean; // 是否還有更多可以加載 pageStart?: number; // 頁面初始頁 initialLoad?: boolean; // 是否第一次就加載 getScrollParent?: () => HTMLElement; //自定義滾動(dòng)容器 useWindow?: boolean; // 是否以 window 作為 scrollEl } class InfiniteScroll extends Component<Props, any> { private scrollComponent: HTMLDivElement | null = null; // 列表數(shù)據(jù) private loadingMore = false; // 是否正在加載更多 private pageLoaded = 0; // 當(dāng)前加載頁數(shù) constructor(props: Props) { super(props); this.scrollListener = this.scrollListener.bind(this); // scrollListener 用到了 this,所以要 bind 一下 } //獲取滾動(dòng)容器 getParentElement(el: HTMLElement | null): HTMLElement | null { const scrollParent = this.props.getScrollParent && this.props.getScrollParent(); if (scrollParent) { return scrollParent; } //默認(rèn)將當(dāng)前組件的外層元素作為滾動(dòng)容器 return el && el.parentElement; } // 滾動(dòng)監(jiān)聽順 scrollListener() { //列表數(shù)據(jù)組件 const node = this.scrollComponent; if (!node) return; //滾動(dòng)容器 const parentNode = this.getParentElement(this.scrollComponent); if (!parentNode) return; let offset; if (this.props.useWindow) { const doc = document.documentElement || document.body.parentElement || document.body; // 全局滾動(dòng)容器 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(); // 加載的時(shí)候去掉監(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; // 獲取滾動(dòng)元素的核心代碼 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;
運(yùn)行結(jié)果:
下滑無限滾動(dòng)
改變loader的位置
offset計(jì)算方法發(fā)生改變:offset = scrollTop
考慮一個(gè)問題:當(dāng)下拉加載新數(shù)據(jù)后滾動(dòng)條的位置不應(yīng)該在scrollY = 0 的位置,不然會(huì)一直加載新數(shù)據(jù)
解決辦法:
當(dāng)前 scrollTop = 當(dāng)前 scrollHeight - 上一次的 scrollHeight + 上一交的 scrollTop parentElement.scrollTop = parentElement.scrollHeight - this.beforeScrollHeight + this.beforeScrollTop
代碼實(shí)現(xiàn):
import * as React from 'react'; import { Component, createElement, ReactNode } from 'react'; interface Props { loadMore: Function; // 加載數(shù)據(jù)的回調(diào)函數(shù) loader: ReactNode; // “加載更多”的組件 threshold: number; // 到達(dá)底部的閾值 hasMore?: boolean; // 是否還有更多可以加載 pageStart?: number; // 頁面初始頁 initialLoad?: boolean; // 是否第一次就加載 getScrollParent?: () => HTMLElement; //自定義滾動(dòng)容器 useWindow?: boolean; // 是否以 window 作為 scrollEl isReverse?: boolean; // 是否為相反的無限滾動(dòng) } class InfiniteScroll extends Component<Props, any> { private scrollComponent: HTMLDivElement | null = null; // 列表數(shù)據(jù) private loadingMore = false; // 是否正在加載更多 private pageLoaded = 0; // 當(dāng)前加載頁數(shù) // isReverse 后專用參數(shù) private beforeScrollTop = 0; // 上次滾動(dòng)時(shí) parentNode 的 scrollTop private beforeScrollHeight = 0; // 上次滾動(dòng)時(shí) parentNode 的 scrollHeight constructor(props: Props) { super(props); this.scrollListener = this.scrollListener.bind(this); // scrollListener 用到了 this,所以要 bind 一下 } //獲取滾動(dòng)容器 getParentElement(el: HTMLElement | null): HTMLElement | null { const scrollParent = this.props.getScrollParent && this.props.getScrollParent(); if (scrollParent) { return scrollParent; } //默認(rèn)將當(dāng)前組件的外層元素作為滾動(dòng)容器 return el && el.parentElement; } // 滾動(dòng)監(jiān)聽順 scrollListener() { //列表數(shù)據(jù)組件 const node = this.scrollComponent; if (!node) return; //滾動(dòng)容器 const parentNode = this.getParentElement(this.scrollComponent); if (!parentNode) return; let offset; if (this.props.useWindow) { const doc = document.documentElement || document.body.parentElement || document.body; // 全局滾動(dòng)容器 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; } // 是否到達(dá)閾值,是否可見 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) { // 更新滾動(dòng)條的位置 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ùn)行結(jié)果
優(yōu)化
1、在mousewheel里通過e.preventDefault解決"加載更多"時(shí)間超長的問題
2、添加被動(dòng)監(jiān)聽器,提高頁面滾動(dòng)性能
3、優(yōu)化render函數(shù)
總結(jié)
無限滾動(dòng)原理的核心就是維護(hù)當(dāng)前的offset值
1、向下無限滾動(dòng):offset = node.scrollHeight - parentNode.scrollTop - parentNode.clientHeight
2、向上無限滾動(dòng):offset = parentNode.scrollTop
3、window為滾動(dòng)容器向下無限滾動(dòng):offset = calculateTopPosition(node) + node.offsetHeight - window.pageYoffset - window.innerHeight
其中calculateTopPosition函數(shù)通過遞歸計(jì)算當(dāng)前窗口頂部距離瀏覽器窗口頂部的距離
4、window為滾動(dòng)容器向上無限滾動(dòng):offset = window.pageYoffset || doc.scrollTop
其中doc = document.documentElement || document.body.parentElement || document.body
到此這篇關(guān)于react無限滾動(dòng)組件的實(shí)現(xiàn)示例的文章就介紹到這了,更多相關(guān)react無限滾動(dòng)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React Native:react-native-code-push報(bào)錯(cuò)的解決
這篇文章主要介紹了React Native:react-native-code-push報(bào)錯(cuò)的解決,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10React?Server?Component混合式渲染問題詳解
React?官方對?Server?Comopnent?是這樣介紹的:?zero-bundle-size?React?Server?Components,這篇文章主要介紹了React?Server?Component:?混合式渲染,需要的朋友可以參考下2022-12-12React?Native?的動(dòng)態(tài)列表方案探索詳解
這篇文章主要為大家介紹了React?Native?的動(dòng)態(tài)列表方案探索示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09React中使用dnd-kit實(shí)現(xiàn)拖曳排序功能
在這篇文章中,我將帶著大家一起探究React中使用dnd-kit實(shí)現(xiàn)拖曳排序功能,由于前陣子需要在開發(fā) Picals 的時(shí)候,需要實(shí)現(xiàn)一些拖動(dòng)排序的功能,文中通過代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2024-06-06React Native實(shí)現(xiàn)簡單的登錄功能(推薦)
這篇文章主要介紹了React Native實(shí)現(xiàn)登錄功能的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09React?state結(jié)構(gòu)設(shè)計(jì)原則示例詳解
這篇文章主要為大家介紹了React?state結(jié)構(gòu)設(shè)計(jì)原則示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06React?高階組件與Render?Props優(yōu)缺點(diǎn)詳解
這篇文章主要weidajai?介紹了React?高階組件與Render?Props優(yōu)缺點(diǎn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11