实现思路
将数据存储为trueData([[data], [data], [data]])和virtualTrueData([[data], [], []])两份,trueData存储所有数据,virtualTrueData内仅和body相交的分页有值,其他分页为空数组,使用高度撑开。
使用 IntersectionObserver 监听每一个分页是否和body相交,相交则将trueData中对应分页值写入virtualTrueData,反之则置空virtualTrueData对应分页值。
在页面加载完成及有新的分页加载完成时获取元素高度并给添加style给父节点撑开高度。
添加触底容器,监听触底事件并调用,获取返回值后入栈trueData及virtualTrueData。
代码展示
1. 组件源码
interface IProps<T> {
data: T[];
key?: string;
maxPages: number;
itemRender: (item: T, index: number, childIndex: number) => JSX.Element;
reachBottom: () => any;
}
type loadProps = 'more' | 'loading' | 'noMore';
function OptimizedReactiveHeight<T>({
data,
key = 'id',
itemRender,
maxPages = 0,
reachBottom = () => {},
}: IProps<T>) {
const isCanCreat = useRef<boolean>(true);
const newPageCreat = useRef<boolean>(false);
const contentObserver = useRef<IntersectionObserver | null>(null);
const footerObserver = useRef<IntersectionObserver | null>(null);
const trueData = useRef<Array<Array<unknown>>>([data]);
const virtualTrueRef = useRef<Array<Array<any>>>([data]);
const statusRef = useRef<loadProps>('more');
const [virtualTrueData, setVirtualTrueData] = useState<Array<Array<any>>>([data]);
const [listStatus, setListStatus] = useState<loadProps>('more');
useLayoutEffect(() => {
if (isCanCreat.current) {
// 创建分页监听
isCanCreat.current = false;
contentObserver.current = new IntersectionObserver(entries => {
if (newPageCreat.current) {
newPageCreat.current = false;
return;
}
console.log('content entries', entries);
entries.forEach(item => {
if (item.intersectionRatio <= 0) {
virtualTrueRef.current[item.target.id] = [];
setVirtualTrueData([...virtualTrueRef.current]);
} else {
virtualTrueRef.current[item.target.id] = trueData.current[item.target.id];
setVirtualTrueData([...virtualTrueRef.current]);
}
});
}, {});
// 脱离更新机制给节点添加高度
setTimeout(() => {
const listEl: any = document.getElementsByClassName('visible-list');
for (let i = 0; i < listEl.length; i++) {
const height = listEl?.[i].offsetHeight || 200;
listEl[i].style.height = `${height}px`;
// @ts-ignore
contentObserver.current?.observe(listEl?.[i]);
}
}, 0);
// 添加触底监听
const footEl = document.getElementsByClassName('list-footer');
footerObserver.current = new IntersectionObserver(entries => {
console.log('footer entries', entries);
// 加载下一页
if (entries[0].intersectionRatio > 0) {
onReachBottom();
}
}, {});
// @ts-ignore
footerObserver.current.observe(footEl[0]);
}
return () => {
// @ts-ignore
contentObserver.current.disconnect();
};
}, []);
const onReachBottom = useCallback(async () => {
if (statusRef.current === 'loading') {
return false;
}
if (statusRef.current === 'noMore') {
return false;
}
setListStatus('loading');
statusRef.current = 'loading';
let res = await reachBottom();
if (res && res.length) {
newPageCreat.current = true;
trueData.current.push(res);
virtualTrueRef.current.push(res);
setVirtualTrueData([...virtualTrueRef.current]);
setTimeout(() => {
const listEl: any = document.getElementsByClassName('visible-list');
const height = listEl[listEl.length - 1].offsetHeight || 2000;
listEl[listEl.length - 1].style.height = `${height}px`;
// @ts-ignore
contentObserver.current.observe(listEl[listEl.length - 1]);
}, 0);
} else {
setListStatus('noMore');
statusRef.current = 'noMore';
}
}, []);
useEffect(() => {
if (virtualTrueData.length >= maxPages) {
setListStatus('noMore');
statusRef.current = 'noMore';
} else {
setListStatus('more');
statusRef.current = 'more';
}
}, [virtualTrueData, maxPages]);
return (
<div className={styles.container}>
<div className="visible-list-box">
{virtualTrueData?.map((item, index) => (
<div key={index} id={String(index)} className="visible-list">
{item &&
item?.map((childItem, childIndex) => (
<div key={childItem[key] || childIndex}>
{itemRender(childItem, index, childIndex)}
</div>
))}
</div>
))}
<div className="list-footer" style={{ minHeight: '100px' }}>
<LoadMore status={listStatus} />
</div>
</div>
</div>
);
}
- loadMore组件参考
const components = { more: <div className={styles.more}>上拉加载</div>,
loading: (
<div className={styles.loading}>
<img
src=""
style={{ width: '44px', height: 'auto' }}
alt="加载中"
/>
</div>
),
noMore: <div className={styles.noMore}>已经到底了~</div>,
};
const LoadMore = ({ status = 'more' }: { status: 'more' | 'loading' | 'noMore' }) => {
return <div className={styles.noSchool}>{components[status]}</div>;
};
- 样式
commonStyle() {
font-size: 14px;
font-weight: 400;
color: #AEB1BD;
line-height: 14px;
height: 14px;
text-align: center;
padding: 40px 0
}
@keyframes turn{
0%{-webkit-transform:rotate(0deg);}
25%{-webkit-transform:rotate(90deg);}
50%{-webkit-transform:rotate(180deg);}
75%{-webkit-transform:rotate(270deg);}
100%{-webkit-transform:rotate(360deg);}
}
.more {
commonStyle();
}
.loading {
commonStyle();
:global(.at-icon-loading) {
animation:turn 1s linear infinite;
}
}
.noMore {
commonStyle()
display: flex;
align-items: center;
justify-content: center;
}
4、使用方式
const listView = useCallback((item, index, childIndex) => {
return (
<div
className={styles.homeList}
onClick={() => {
toDetail(item, index, childIndex);
}}
>
{index}
</div>
);
}, []);
// 页面触底事件
const reachBottom = useCallback(async () => {
let res = await getList().catch(err => {
console.log('reachBottom err---->', err);
return [];
});
return res;
}, []);
// data及reachBottom触底事件均返回list的对象数组即可
<HappyList
data={data}
maxPages={pagesNum}
itemRender={listView}
reachBottom={reachBottom}
/>
目前存在的问题
maxPage不支持动态传入(解决中)
删除/刷新节点未实现(解决中)
兼容性问题
作者:zzppff
git链接待更新
原创方法,商业转载请联系作者获得授权,非商业转载请注明出处。