字
字节笔记本
2026年2月22日
手把手实现 React 无限滚动组件
本文介绍 my-react-infinite-scroller,一个手把手教你实现 React 无限滚动组件的开源教学项目。该项目不仅提供了完整的无限滚动实现代码,还详细讲解了 scrollHeight、scrollTop、clientHeight 等核心概念,帮助开发者深入理解无限滚动的底层原理。
项目简介
my-react-infinite-scroller 是由 haixiangyan 开发的一个开源教学项目,旨在帮助开发者从零开始理解并实现 React 无限滚动组件。与直接使用现成的 npm 包不同,这个项目通过循序渐进的代码示例,让你彻底搞懂无限滚动的核心原理。
截至目前,该项目在 GitHub 上已获得 13 stars,包含 21 次提交,采用 TypeScript 编写,并配有详细的图文教程。
核心特性
- 循序渐进的学习路径:从最小实现开始,逐步添加功能
- 完整的 TypeScript 支持:类型安全的代码实现
- 双向无限滚动:支持向下和向上(反向)无限滚动
- 全局/局部滚动容器:支持 window 和自定义容器两种滚动模式
- 详细的原理解析:图解 scrollHeight、scrollTop、clientHeight 等关键概念
- 实际可运行的 Demo:提供在线预览和本地运行示例
技术栈
- React - 核心 UI 框架
- TypeScript - 类型安全的 JavaScript 超集
- CSS - 样式处理
核心原理:offset 公式
无限滚动的核心原理很简单:当「剩余可滚动距离」小于设定的阈值时,触发加载更多数据的操作。
计算公式
typescript
const offset = node.scrollHeight - parentNode.scrollTop - parentNode.clientHeight
if (offset < this.props.threshold) {
this.props.loadMore()
}关键概念解释
| 属性 | 说明 |
|---|---|
scrollHeight | 元素的内容高度,包括溢出导致的不可见内容 |
scrollTop | 元素内容垂直滚动的像素数 |
clientHeight | 元素的可视高度(包含 padding) |
安装与使用
克隆项目
bash
git clone https://github.com/haixiangyan/my-react-infinite-scroller.git
cd my-react-infinite-scroller安装依赖
bash
npm install
# 或
yarn install启动项目
bash
npm start
# 或
yarn start访问 http://localhost:3000 查看效果。
快速开始
基础用法
typescript
import React, { useState, useEffect } from 'react'
import InfiniteScroll from './InfiniteScroll'
let counter = 0
const delay = (asyncFn: () => Promise<void>) =>
new Promise<void>((resolve) => {
setTimeout(() => {
asyncFn().then(() => resolve)
}, 1500)
})
const App = () => {
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>
<div style={{ height: 250, overflow: 'auto', border: '1px solid red' }}>
<InfiniteScroll
threshold={50}
loadMore={fetchMore}
loader={<div className="loader" key={0}>Loading ...</div>}
>
{items.map((item) => (
<div key={item}>{item}</div>
))}
</InfiniteScroll>
</div>
</div>
)
}
export default App核心实现解析
最小实现版本
typescript
interface Props {
loadMore: Function
loader: ReactNode
threshold: number
hasMore?: boolean
pageStart?: number
}
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() {
const node = this.scrollComponent
if (!node) return
const parentNode = node.parentElement
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
}
}
componentDidMount() {
this.pageLoaded = this.props.pageStart || 0
this.attachScrollListener()
}
componentDidUpdate() {
this.attachScrollListener()
}
componentWillUnmount() {
this.detachScrollListener()
}
attachScrollListener() {
const parentElement = this.getParentElement(this.scrollComponent)
if (!parentElement) return
parentElement.addEventListener('scroll', this.scrollListener)
parentElement.addEventListener('resize', this.scrollListener)
}
detachScrollListener() {
const parentElement = this.getParentElement(this.scrollComponent)
if (!parentElement) return
parentElement.removeEventListener('scroll', this.scrollListener)
parentElement.removeEventListener('resize', this.scrollListener)
}
getParentElement(el: HTMLElement | null): HTMLElement | null {
return el && el.parentElement
}
render() {
const { children, loader } = this.props
return (
<div ref={(node) => (this.scrollComponent = node)}>
{children}
{loader}
</div>
)
}
}支持 Window 全局滚动
当需要在全局(window)实现无限滚动时,需要使用另一套计算公式:
typescript
// 计算元素顶部到页面顶部的距离
calculateTopPosition(el: HTMLElement | null): number {
if (!el) return 0
return el.offsetTop + this.calculateTopPosition(el.offsetParent as HTMLElement)
}
// 计算 offset
calculateOffset(el: HTMLElement | null, scrollTop: number) {
if (!el) return 0
return this.calculateTopPosition(el) + el.offsetHeight - scrollTop - window.innerHeight
}支持反向无限滚动
反向无限滚动(向上滚动加载更多)常用于聊天记录等场景:
typescript
// 在 scrollListener 中处理反向滚动
let offset
if (this.props.useWindow) {
const doc = document.documentElement || document.body.parentElement || document.body
const scrollTop = window.pageYOffset || doc.scrollTop
offset = this.props.isReverse ? scrollTop : this.calculateOffset(el, scrollTop)
} else {
offset = this.props.isReverse
? parentElement.scrollTop
: el.scrollHeight - parentElement.scrollTop - parentElement.clientHeight
}API 参考
Props
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
loadMore | Function | 是 | 加载更多数据的回调函数 |
loader | ReactNode | 是 | 加载状态显示的组件 |
threshold | number | 是 | 触发加载的阈值(像素) |
hasMore | boolean | 否 | 是否还有更多数据可加载 |
pageStart | number | 否 | 初始页码,默认为 0 |
useWindow | boolean | 否 | 是否使用 window 作为滚动容器 |
isReverse | boolean | 否 | 是否为反向无限滚动 |
getScrollParent | () => HTMLElement | 否 | 自定义获取滚动容器的函数 |
项目链接
- GitHub 仓库: https://github.com/haixiangyan/my-react-infinite-scroller
- 在线预览: http://yanhaixiang.com/my-react-infinite-scroller/
- 参考轮子: https://www.npmjs.com/package/react-infinite-scroller
总结
my-react-infinite-scroller 是一个非常优秀的开源教学项目,它不仅提供了可运行的代码,更重要的是通过详细的图文教程帮助开发者理解无限滚动的底层原理。如果你想深入理解 React 组件开发或者需要自定义无限滚动功能,这个项目绝对值得学习。
分享: