likes
comments
collection
share

手撸一个InfiniteScroll无限滚动加载组件

作者站长头像
站长
· 阅读数 15

好像有一阵没更新 UI组件 专栏了,正巧也赶上最近在阅读 react-infinite-scroll-component 的源码,于是就写了这篇文章来给自己一个正向反馈。

我们先来看一下实现的效果吧:

手撸一个InfiniteScroll无限滚动加载组件

随着我们不断的向下滚动,右侧的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阈值

手撸一个InfiniteScroll无限滚动加载组件

如上图,蓝色部分代表着父容器的视口,上面我们分析过,父容器必须向下滚动一定的距离才能去触发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
评论
请登录