手撸一个InfiniteScroll无限滚动加载组件
好像有一阵没更新 UI组件
专栏了,正巧也赶上最近在阅读 react-infinite-scroll-component
的源码,于是就写了这篇文章来给自己一个正向反馈。
我们先来看一下实现的效果吧:
随着我们不断的向下滚动,右侧的dom数量也在不断的增加。
根据这个效果,我们来看一下这个 react-infinite-scroll-component
库的用法:
import InfiniteScroll from 'react-infinite-scroll-component';
export default class RList extends React.Component {
constructor(props){
super(props);
this.state = {
currentData: [0, 1, 2]
}
}
// 获取更多数据
fetchMoreData = () => {
this.setState(state => {
return {
currentData: state.currentData.concat(state.currentData.length + 1, state.currentData.length + 2, state.currentData.length + 3)
}
});
}
render(){
let { currentData, allDataCount } = this.state;
let self = this;
return <div style={{ height: '600px', width: '300px', border: '1px solid #ccc' }}>
<InfiniteScroll
dataLength={currentData?.length}
next={self.fetchMoreData}
hasMore={true}
height={600}
>
{
currentData && currentData?.map((item, index) => (
<div style={{ width: '100%', height: '300px' }>
{item}
</div>
))
}
</InfiniteScroll>
</div>
}
}
首先,如果我们想要看到滚动加载的效果,那么初始列表的总高度就一定要大于InfiniteScroll组件的height属性值
(这是先决条件)。在上面的代码里,currentData的初始长度是3,每个列表项的高度是300px,而我们给InfiniteScroll组件的height属性值是600px,因为3*300 > 600,所以我们符合了先决条件。
其次,既然是无限滚动,说明要加载更多的数据,我们要告诉InfiniteScroll组件有更多的数据要进来,所以需要将InfiniteScroll组件的hasMore属性值设为true
。
最后,用户需要定义一个方法,这个方法用来实现不断的push数据。但是调用这个方法不是用户手动调用,而是交给InfiniteScroll组件,因此我们需要将InfiniteScroll组件的next属性值设为刚才的方法
。
源码分析
这个库的整体结构很简单,主代码都在src下的index.tsx:
// props属性的类型声明
interface Props {
...
}
// state状态的类型声明
interface State {
...
}
export default class InfiniteScroll extends Component<Props, State> {
// 组价初始化
componentDidMount(){}
// 更新的第一步,判断是否需要更新
static getDerivedStateFromProps(nextProps, prevState){}
// 更新后
componentDidUpdate(prevProps){}
// 组件即将卸载
componentWillUnmount(){}
// 获取滚动容器
getScrollableTarget = () => {}
// 判断何时加载数据
isElementAtBottom = () => {}
// 滚动监听事件
onScrollListener = () => {}
render(){
return <div
className="infinite-scroll-component__outerdiv"
>
<div
className={`infinite-scroll-component}`}
ref={(infScroll) => (this._infScroll = infScroll)}
style={this.props.style}
>
{this.props.children}
</div>
</div>
}
}
1.1、componentDidMount
import { throttle } from 'throttle-debounce';
this.throttledOnScrollListener = throttle(150, this.onScrollListener).bind(
this
);
componentDidMount(){
// 1、dataLength属性必传,如果没有,则抛错
if (typeof this.props.dataLength === 'undefined') {
throw new Error(
`mandatory prop "dataLength" is missing. The prop is needed` +
` when loading more content. Check README.md for usage`
);
}
// 2、获取滚动容器
this.el = this._infScroll;
// 3、对滚动容器 设置 滚动监听
if (this.el) {
this.el.addEventListener('scroll', this.throttledOnScrollListener);
}
}
1.2、getDerivedStateFromProps
static getDerivedStateFromProps(nextProps, prevState){
// 触发更新说明数据长度发生了改变,如果没变,就不触发更新
const dataLengthChanged = nextProps.dataLength !== prevState.prevDataLength;
if (dataLengthChanged){
return {
...prevState,
prevDataLength: nextProps.dataLength
}
}
return null;
}
1.3、componentDidUpdate
componentDidUpdate(prevProps){
if (this.props.dataLength === prevProps.dataLength) return;
// 走到这里说明next函数执行完毕,需要reset next函数执行标识
this.actionTriggered = false;
}
1.4、componentWillUnmount
componentWillUnmount(){
// 移除scroll监听事件
if (this.el){
this.el.removeEventlistener('scroll', this.throttledOnScrollListener)
}
}
1.5、onScrollListener
onScrollListener = (event) => {
// 1、获取滚动容器
const target = event.target;
// 2、只要next函数被触发,this.actionTriggered就为true,在这块判断是为了防止next被多次触发
if (this.actionTriggered) return;
// 3、判断是否可以到达阈值
const atBottom = this.isElementAtBottom(target, this.props.scrollThreshold);
// 4、如果到达阈值并且hasMore为true,则触发next函数,进行数据的push
if (atBottom && this.props.hasMore) {
this.actionTriggered = true;
this.props.next && this.props.next();
}
}
1.6、什么是阈值
我们都知道,滚动的产生是因为子元素的高度超过了父元素。那这里就有一个问题,我们什么时候加载数据?
这里肯定有人会说,那当然是hasMore为true的时候来push数据啊,这句话没错,但是它缺少了滚动距离
的概念。
我们不可能滚动一点就加载数据
吧。就是我们不可能连一条数据都没滚动完就加载数据。
因此从滚动距离
的维度上看,我们有2种方式来控制next函数的执行。一种是判断 scrollTop的值大于某一范围才执行next函数
;另一种是判断上一轮数据的最后一项出现在视窗内,再去执行next函数
。
在这里我们讲解第一种方式
1.6.1、scrollTop阈值
如上图,蓝色部分代表着父容器的视口,上面我们分析过,父容器必须向下滚动一定的距离才能去触发next函数,这样也更合理。
根据上图观察,scrollTop的合理值应该如下:
父容器.scrollTop + 父容器.clientHeight >= percent * 父容器.scrollHeight
。
解释:父容器显示过多少条 + 父容器正在显示的条数 >= 父容器里总条数的百分比。
其中,有关clientHeight、scrollHeight的知识点不清晰的同学请看这篇文章:《你真的了解前端里的各种高度吗?》
1.6.2、isElementAtBottom
isElementAtBottom
的源码就是咱们上面分析阈值合理范围的过程。
function isElementAtBottom( target, scrollThreshold = 0.8){
const clientHeight = target.clientHeight;
return (
target.scrollTop + clientHeight >= scrollThreshold * target.scrollHeight
);
}
最后
好啦,本篇UI组件实现系列到这里就结束啦。读过以前的文章的同学会发现,这篇跟其他篇不太一样,这篇主要是带着大家一步步的看懂现有的第三方库的源码,从而让读者自己实现一个这样的无限加载的组件。如果我写的对你有帮助,希望大家多多点赞支持一下,如果上述存在错误,还请大家指正,那么,我们下期再见啦~~
转载自:https://juejin.cn/post/7253102401451671607