继第一版滚动加载之后,后来整理了一下,发现冗余代码比较多。决定返璞归真,从新整理了现在的思路 ,由简入繁
思考
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 };
};