react無限滾動組件的實現(xiàn)示例
上拉無限滾動
核心:判斷滾動條是否觸底了,觸底了就重新加載數(shù)據(jù)
判斷觸底:scrollHeight-scrollTop-clientHeight<閾值
容器底部與列表底部的距離(表示還剩多少px到達底部)=列表高度-容器頂部到列表頂部的距離-容器高度

說一下幾個概念
scrollHeight:只讀屬性。表示當前元素的內(nèi)容總高度,包括由于溢出導(dǎo)致在視圖中不可見的內(nèi)容。這里獲取的是列表數(shù)據(jù)的總高度
scrollTop:可以獲取或設(shè)置一個元素的內(nèi)容垂直滾動的像素數(shù)。這里獲取的是容器頂部到列表頂部的距離,也就是列表卷去的高度

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

代碼實現(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; // 到達底部的閾值
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);
//設(shè)置滾動條即時不動也會自動觸發(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;運行結(jié)果:

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ù)的回調(diào)函數(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;運行結(jié)果:

下滑無限滾動

改變loader的位置

offset計算方法發(fā)生改變:offset = scrollTop

考慮一個問題:當下拉加載新數(shù)據(jù)后滾動條的位置不應(yīng)該在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ù)的回調(diào)函數(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;運行結(jié)果

優(yōu)化
1、在mousewheel里通過e.preventDefault解決"加載更多"時間超長的問題
2、添加被動監(jiān)聽器,提高頁面滾動性能
3、優(yōu)化render函數(shù)
總結(jié)
無限滾動原理的核心就是維護當前的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
到此這篇關(guān)于react無限滾動組件的實現(xiàn)示例的文章就介紹到這了,更多相關(guān)react無限滾動內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React Native:react-native-code-push報錯的解決
這篇文章主要介紹了React Native:react-native-code-push報錯的解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10
React?Server?Component混合式渲染問題詳解
React?官方對?Server?Comopnent?是這樣介紹的:?zero-bundle-size?React?Server?Components,這篇文章主要介紹了React?Server?Component:?混合式渲染,需要的朋友可以參考下2022-12-12
React Native實現(xiàn)簡單的登錄功能(推薦)
這篇文章主要介紹了React Native實現(xiàn)登錄功能的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-09-09
React?state結(jié)構(gòu)設(shè)計原則示例詳解
這篇文章主要為大家介紹了React?state結(jié)構(gòu)設(shè)計原則示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06
React?高階組件與Render?Props優(yōu)缺點詳解
這篇文章主要weidajai?介紹了React?高階組件與Render?Props優(yōu)缺點詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11

