继上次优化之后效果产品还是不满意,所以又优化了一个版本,在iphone手机上基本做到了下拉秒加载,在红米9pro上测试效果也很好;不过有个上限,当上拉加载的商品超过200个的时候性能会有所下降;
新的方案
- 每页10个商品,上拉加载下一页商品
- 每页10个商品是一组,每组是一个独立虚拟卡片,当该组商品不在屏幕中时,会被一张站位图替换
- 相比之前的虚拟列表方案
- 增加了分页,之前的是以商品维度的虚拟卡片,滑动的时候会频繁的渲染,并且商品卡片是不超过一屏幕的,空白站位图频繁出现
代码
/**
* 可分组的虚拟列表
*/
import { useEffect, useRef, useState } from 'react';
import { ScrollView, View } from '@tarojs/components';
import { AtActivityIndicator } from 'taro-ui';
import VirtualListItem from './virtualListItem';
interface VirtualScrollerListProps {
/** 数据 */
dataSource: any[];
/** 分页每页多少 */
pageSize: number;
onScrollToUpper: () => Promise<void>;
onScrollToLower: () => Promise<void>;
[key: string]: any
}
/**
* 将一维数组转换为二维数组
*
* @param arr 一维数组
* @param rows 转换后的二维数组的行数
* @returns 转换后的二维数组
*/
function convertTo2DArray(arr, pageSize) {
var numRows = Math.ceil(arr.length / pageSize);
var result: any = [];
for (var i = 0; i < numRows; i++) {
var startIdx = i * pageSize;
var endIdx = startIdx + pageSize;
var row = arr.slice(startIdx, endIdx);
result.push(row);
}
console.log(result);
return result;
}
export default function VirtualScrollerList({ dataSource, pageSize, onScrollToLower, onScrollToUpper, goodsType, orderType, activeClassifyType, ...lastProps }: VirtualScrollerListProps) {
const [list, setList] = useState<any[]>([]);
const scrollDirectionRef = useRef<'up' | 'down'>('down');
const touchPosition = useRef<any>({}); // 上拉加载
const [refresherTriggered, setRefresherTriggered] = useState(false); // 滚动列表的参数
// const [scrollKey, setScrollKey] = useState('k' + String(Date.now()))
const scrollKeyRef = useRef('k' + String(Date.now()))
const loadingRef = useRef(false);
const [paging, setPaging] = useState<{
currnet: number,
total: number
pageSize: number
}>({
currnet: 1,
total: 0,
pageSize: 10
})
useEffect(() => {
// 这个做二维分组
const newList = convertTo2DArray(dataSource, pageSize);
console.log(newList);
Object.assign(paging, {
currnet: 1,
total: newList.length,
pageSize: pageSize
})
scrollKeyRef.current = 'k' + String(Date.now());
setList(newList)
}, [dataSource])
const onScrollToUpperFn = async () => {
scrollDirectionRef.current = 'down';
await onScrollToUpper()
setRefresherTriggered(false); // 下拉刷新状态置为false
};
const onScrollToLowerFn = () => {
console.log('onScrollToLower');
if (loadingRef.current) return;
loadingRef.current = true;
setPaging({
...paging,
currnet: paging.currnet + 1,
})
hideLoading();
}
const onTouchEnd = () => {
console.log('onTouchEnd', touchPosition.current);
if (loadingRef.current) return;
const { start, end } = touchPosition.current;
if (start && end && start.pageY - end.pageY >= 100 && paging.currnet >= paging.total) {
loadingRef.current = true;
console.log('next');
onScrollToLower();
hideLoading();
}
};
const hideLoading = () => {
setTimeout(() => {
loadingRef.current = false;
}, 500)
}
return (
<>
<ScrollView
key={scrollKeyRef.current}
scrollY
scrollX={false}
style={{ height: '100%' }}
lowerThreshold={200}
refresherThreshold={50}
refresherEnabled
refresherBackground='#F6F6F6'
refresherTriggered={refresherTriggered}
onScrollToLower={onScrollToLowerFn}
refresherDefaultStyle='none'
onRefresherPulling={() => {
setRefresherTriggered(true);
}}
onRefresherRefresh={onScrollToUpperFn}
onTouchStart={e => {
console.log('onTouchStart');
touchPosition.current = {
start: e.touches[0]
};
}}
onTouchMove={e => {
console.log('onTouchMove')
touchPosition.current = {
...touchPosition.current,
end: e.touches[0]
};
}}
onTouchEnd={onTouchEnd}
>
<View
style={{
position: 'absolute',
width: '100%',
height: '60px',
background: '#F6F6F6',
display: 'flex',
alignItems: 'center',
top: '-60px',
textAlign: 'center',
color: '#999999',
justifyContent: 'center'
}}>
下拉加载更多
</View>
{
(list.slice(0, paging.currnet)).map((subList: any, index: number) => {
return (
<VirtualListItem observeIdKey={`observe__${scrollKeyRef.current}_${index}`} key={index}>
{/* 商品卡片 */}
<CustomComponent></CustomComponent>
</VirtualListItem>
)
})
}
<View key={paging.currnet} style={{ color: '#999999', textAlign: 'center', padding: '12rpx', display: 'flex', width: '100%', justifyContent: 'center', alignItems: 'center' }}>
{paging.currnet >= paging.total ? '没有更多了' : <AtActivityIndicator content='加载中...'></AtActivityIndicator>}
</View>
</ScrollView>
</>
);
}
/*
* 虚拟卡片
*/
import { CustomWrapper, View } from "@tarojs/components";
import Taro from "@tarojs/taro";
import { useEffect, useRef, useState } from "react";
import { wxuuid } from "@/utils/helper";
interface VirtualListItemProps {
children: React.ReactNode;
observeIdKey?: string // 用于性能优化
}
export default function VirtualListItem({ children, observeIdKey }: VirtualListItemProps) {
const observeId = observeIdKey || `observe_${wxuuid()}`
const height = useRef(0);
const [visible, setVisible] = useState(true);
const timeoutRef = useRef<any>();
useEffect(() => {
let ob = Taro.createIntersectionObserver(this, {
observeAll: true
});
timeoutRef.current = setTimeout(() => {
ob.relativeToViewport().observe(`.${observeId}`, res => {
height.current = res.boundingClientRect.height;
const show = res.intersectionRatio !== 0;
setVisible(show);
});
}, 10);
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
ob.disconnect();
};
}, [observeId]);
return (
// CustomWrapper 放在最外层导致observe监听不到,所以放在最内层
<View
id={observeId}
className={observeId}
style={{
width: "100%",
height: height.current || '',
background: visible ? "none" : "url(https://oss-cdn.mkh.cn/fe/image/2021-10-22/c178f22e-d5e5-4742-a0bf-cb8831ec505b.png) repeat-y",
backgroundSize: "100% 400rpx"
}}
>
{/* 这种写法有问题,包裹后更新visible无效 */}
{/* <CustomWrapper>
{visible && children}
</CustomWrapper> */}
{
visible && (
<CustomWrapper>
{children}
</CustomWrapper>
)
}
</View>
);
}
总结
可以看出这种方式和之前的方式比较,随着下拉数据的增多会多很多createIntersectionObserver监听对象,意味着消耗的性能更多,如果把pageSize设置为1,就是我尝试过最早的一个版本,会为每个商品卡片创建一个简体对象;因为产品是内部产品,视频放不了;