实现上划、下拉加载接口(第二版) (react,hook,IntersectionObserver)

286 阅读5分钟

继第一版滚动加载之后,后来整理了一下,发现冗余代码比较多。决定返璞归真,从新整理了现在的思路 ,由简入繁

思考

1.在滚动的时候,只需要IntersectionObserver触发的时候,令pageNo=pageNo+1就行 所以又写了一个hooks ,用来返回当前的页码 --->useScrollPageNo。

2.既然我们可以拿到滚动的pageNo了 ,可以传入一个异步方法,省去一部分重复的代码--->useScrollDataWithRef2 和(第一版的useScrollDataWithRef是一样的结构,只不过加了一个callback 用来表示滚动加载需要执行的异步方法)

useScrollPageNo和useScrollDataWithRef2 是同样功能的方法,在使用的时候选择其一就可以(推荐使用 useScrollDataWithRef2 ) useScrollPageNo是用来理解整个过程的

import { useEffect, useRef, useState } from 'react';
import { IScrollRefs } from '.';

/**
 * useScrollPageNo 获取当前页数
 *
 * 例:js
 * import { mockList } from '../SystemSetting/action';
 *
 * let [refs, pageNo]= useScrollPageNo()
 * mockList(pageNo).then(res=>{res})
 */

/**
 *
 * @returns IScrollRefs
 * @returns number 当前页数
 */
export const useScrollPageNo = (): [IScrollRefs, number] => {
    const observerRef = useRef<any>({});
    const refs: IScrollRefs = useScrollRefs();
    const [pageNo, setPageNo] = useState(1);
    const initObserver = () => {
        const observeCallback = (entries: any) => {
            for (let item of entries) {
                if (item.isIntersecting) {
                    setPageNo((num) => {
                        console.log(num, 'num');

                        return num + 1;
                    });
                }
            }
        };
        const options = {
            root: refs?.root?.current,
            rootMargin: '200px',
            threshold: [0.1],
        };
        const observer = new IntersectionObserver(observeCallback, options);
        observerRef.current.observer = observer;
    };

    useEffect(() => {
        initObserver();
    }, []);
    useEffect(() => {
        observerRef?.current?.observer?.disconnect();
        if (typeof refs.target === 'object') {
            refs?.target && observerRef?.current?.observer?.observe(refs.target.current);
        }
        return () => observerRef?.current?.observer?.disconnect();
    }, [refs?.target]);
    return [refs, pageNo];
};

IntersectionObserver 是浏览器自带的一个api ,它需要两个dom元素 一个是root,一个是目标对象target。root是用来标示当前整个监视区域的根结点。target是observer观测的目标对象,只要target出现在root规定的范围内,observeCallback 回调方法就会触发。所以这里使用了useScrollRefs 来获取root、target两个dom 元素

也可以useScrollPageNo('classRoot','classTarget'),然后通过querySelecter 来获取dom元素


...
   export const useScrollRefs = (): IScrollRefs => {
    const scrollRef = useRef(null);
    const [refss, setRefss] = useState<IRefss>();
    useEffect(() => {
        const refs = scrollRef?.current?.getRefs();
        setRefss(refs);
    }, []);

    return { scrollRef, root: refss?.rootRef, target: refss?.targetRef };
};
...

如果没有看上一篇文章,可能会对 scrollRef?.current?.getRefs()这里有困惑。想一下,我们到目前为止,实现了通过IntersectionObserver 来检测一个dom元素,然后触发一个回调函数,我们在回调函数中实现了 pageNo=pageNo+1 的效果。

但是没有实现root,target 的布局scrollRef?.current?.getRefs()就是帮我们获取到root和target的,当然可以使用querySelecter来获取dom。上面我们也提到过。现在就是(root,target)布局+useScrollPageNo,才能实现滚动加载的能力,

要分清楚,这是两部分的工作(一个是滚动布局,一个是滚动pageNo+1。不是混合在一起的) 下面就是滚动布局的代码

import React, { forwardRef, useEffect, useRef, useImperativeHandle, useContext } from 'react';
import './index.less';
import { IScrollParamRef } from './type';
import { IItem } from '../../hooks';

/**
 *
 * @param param
 * @param param.list 已经渲染的全部数据
 * @param param.setList 滚动加载的数据上抛(可以想象成 onChange 事件)
 * @param param.renderItem 滚动加载的 每条数据的展示
 * @param param.type 向上滚动|向下滚动加载
 * @param param.data 滚动加载的请求数据
 * @returns
 */
const ScrollListRef = forwardRef((props: IScrollParamRef, refparams) => {
    const { list = [], setList, renderItem, type = 'bottom', data = [] } = props;
    const rootRef = useRef(null);
    const topRef = useRef(null);
    const bottomRef = useRef(null);
    const targetRef = type === 'top' ? topRef : bottomRef;

    useImperativeHandle(
        refparams,
        () => {
            return {
                getRefs: () => ({ rootRef, targetRef }),
            };
        },
        [refparams]
    );

    useEffect(() => {
        if (type === 'bottom') {
            if (Array.isArray(data) && data.length > 0) {
                setList &&
                    setList((list = []) => {
                        return [...list, ...data];
                    });
            } else {
                console.error('请检查 data 的格式-', data);
            }
        } else {
            if (data?.length > 0) {
                let lastele: IItem = data?.pop();
                lastele!.indexTag = 'lastTag';
                data.push(lastele);
                setList &&
                    setList((list: Array<IItem>) => {
                        let arr = list.map((item: IItem) => {
                            if (item.indexTag) {
                                const { indexTag, ...other } = item;
                                return other;
                            } else {
                                return item;
                            }
                        });
                        return [...data, ...arr];
                    });
            }
        }
    }, [data]);

    useEffect(() => {
        if (list?.length > 0 && type === 'top') {
            rootRef?.current?.querySelector('.last-tag')?.scrollIntoView?.(true);
        }
    }, [list?.length]);

    return (
        <div className={`root ${props?.rootClass}`} style={props?.rootStyle} ref={rootRef}>
            {type === 'top' ? (
                <div className={`top ${props?.topClass}`} style={props?.topStyle} ref={topRef}>
                    {/* 我是有顶线的 - {state?.chatDataNow?.groupId} */}
                </div>
            ) : null}
            {list?.map((item: IItem, index: number) => (
                <div key={'index' + index} className={`${item?.indexTag === 'lastTag' ? 'last-tag' : ''}`}>
                    {renderItem(item, index)}
                </div>
            ))}
            {type === 'bottom' ? (
                <div className={`bottom ${props?.bottomClass}`} style={props?.bottomStyle} ref={bottomRef}>
                    {/* 我是有底线的 */}
                </div>
            ) : null}
        </div>
    );
});
export default ScrollListRef;

这里规定了 root,target(top,bottom,分为向上划动或者向下划,通过type来控制),也通过useImperativeHandle实现了scrollRef?.current?.getRefs(),即两个同级组件中能互相调用方法。现在来看看具体怎么结合业务来使用

import React, { useEffect, useRef, useState } from 'react';

import './index.less';

import { useScrollPageNo } from '../components/hooks/hooks';
import { IScrollParam, IScrollParamRef } from '../components/component/ScrollList/type';
import { IItem } from '../components/hooks'; // 可继承复写

import { ScrollListRef } from '../components/component/ScrollList';

import { mockList } from '../SystemSetting/action';

export default function IndexPage() {

    const [list, setList] = useState([]); // list 是全部请求的全部数据

    // 使用 useScrollPageNo
     const [refs, pageNo] = useScrollPageNo();
     const [data, setData] = useState([]); // data 是每一次请求的单页数据

     useEffect(() => {
         // mockList 就是异步请求
         mockList(pageNo).then((res) => {
             setData(res);
         });
    }, [pageNo]);

    const scrollParam2: IScrollParamRef = {
        list,
        setList,
        type: 'top',
        renderItem: (item: IItem, index: number) => {
            return (
                <div style={{ height: '150px' }}>
                    {item.value}:+
                    为了理解中间件,让我们站在框架作者的角度思考问题:如果要添加功能,你会在哪个环节添加?
                    Reducer:纯函数,只承担计算 State
                </div>
            );
        },
        data,
        rootStyle: { height: '50vh' },
        ref: refs.scrollRef,
    };

   

    return <ScrollListRef {...scrollParam2}></ScrollListRef>
}



以上就是使用 useScrollPageNo 实现滚动加载

下面是useScrollDataWithRef2 的实现方法

import { useState, useEffect, useRef } from 'react';
import { IScrollRefs } from '.';

interface IResetStateParam {
    pageNo?: number;
    hasNext?: boolean;
}
type TCallback = (pageNo: number) => Promise<any>;
type resetState = (param: IResetStateParam) => void;

/**
 * let [refs, data]= useScrollDataWithRef2((pageNo) => mockList(pageNo))  等同于
 * let [refs, data]= useScrollDataWithRef({ callback:(pageNo) => mockList(pageNo)})
 * 可放心混用
 */

/**
 * 例:js
 * let [refs, data,resetState]= useScrollDataWithRef2((pageNo) => mockList(pageNo))
 ** resetState 重置useScrollDataWithRef的页码 以及返回的数据

 * 例:dom  ScrollListRef
 * const scrollParam2: IScrollParamRef = {
 *      list,
 *      setList,
 *      type: 'top',
 *      renderItem: (item: IItem, index: number) => <span>random</span>,
 *      data,
 *      rootStyle: { height: '60vh' },
 *      ref: refs.scrollRef,
 *  };
 *
 * <ScrollListRef {...scrollParam2}></ScrollListRef>
 *
 * @param callback 异步请求的promise 方法
 * @returns [refs, result, resetState]
 * @returns refs 如上例
 * @returns result 异步请求返回的结果 可以根据数据结构自行处理 result.list  ||result.data
 * @returns resetState 重置或者设置 pageNo
 */
export const useScrollDataWithRef2 = (callback: TCallback): [IScrollRefs, any, resetState] => {
    const [result, setResult] = useState<any>();
    const observerRef = useRef<any>({});
    const refs: IScrollRefs = useScrollRefs();
    const pageNoRef = useRef(1);
    const hasNextRef = useRef(true);

    const initObserver = () => {
        const observeCallback = (entries: any) => {
            for (let item of entries) {
                if (item.isIntersecting) {
                    if (hasNextRef.current) {
                        callback(pageNoRef.current).then((res) => {
                            setResult(res);
                        });
                        pageNoRef.current++;
                    }
                }
            }
        };
        const options = {
            root: refs?.root?.current,
            rootMargin: '200px',
            threshold: [0.1],
        };
        const observer = new IntersectionObserver(observeCallback, options);
        observerRef.current.observer = observer;
    };

    const resetState = (param: IResetStateParam) => {
        const { pageNo = 1, hasNext = true } = param;
        pageNoRef.current = typeof pageNo === 'number' ? pageNo : 1;
        hasNextRef.current = typeof hasNext === 'boolean' ? hasNext : true;
        // setResult({});
    };
    useEffect(() => {
        console.log('initObserver 重新注册 当前page' + pageNoRef.current);
        initObserver();
        return () => resetState({ pageNo: 1, hasNext: true });
    }, []);
    useEffect(() => {
        observerRef?.current?.observer?.disconnect();
        if (typeof refs.target === 'object') {
            refs?.target && observerRef?.current?.observer?.observe(refs.target.current);
        }
        return () => observerRef?.current?.observer?.disconnect();
    }, [refs?.target]);
    return [refs, result, resetState];
};

interface IRefss {
    rootRef: HTMLDivElement;
    targetRef: HTMLDivElement;
}
export const useScrollRefs = (): IScrollRefs => {
    const scrollRef = useRef(null);
    const [refss, setRefss] = useState<IRefss>();
    useEffect(() => {
        const refs = scrollRef?.current?.getRefs();
        setRefss(refs);
    }, []);

    return { scrollRef, root: refss?.rootRef, target: refss?.targetRef };
};