原理就和我在那篇评论里留言的差不多,就是每次只显示固定的几个div,滚动时把div给换掉。
这东西实现起来难点有下面几个:
一、滚动条的出现
二、可视区域流畅移动
三、闪屏优化
这玩意就不按步骤写了,主要说一下这几个难点是怎么解决的:
滚动条出现 这个难点最简单,但是思路没打开就容易想不到这种虚拟化实现方法。靠空白的div做的滚动条。 这主要还是和布局有关,目前有几种布局可以参考: 第一种就是滚动条和列表并排,这样完全就可以撑起来了。 第二种是滚动条打底,列表项绝对定位,因为本来列表项就需要进行移动来配合滚动条,所以列表项绝对定位也是可以的。 我组件里采用第一种方式。 可视区域流畅移动第二个难点在可视区域的移动上,因为按虚拟化的方案,可视区域是和滚动条一起走的,但是还要兼顾起始项高度与每一项的高度,不然就变成可视区域fixed在屏幕上的奇怪现象。
为了滚动更加流畅,还需要估算一个滚动到每个元素的百分比,不然会出现可视区域瞬间所有项目改变的奇怪现象。百分比使用余数进行计算,可以产生滚动条滚在当前元素的百分之几,然后这个百分比乘每行高度得到需要减去的距离。
闪屏优化 这个是最难的,耗费我好几个小时,还走了很多误区。 解决了前面2个难点,基本上已经可以工作了,但是会出现闪屏。 根据我打断点发现,闪屏原因是监听scroll的函数里设置索引,然后刷新页面,索引改动后,useEffect触发执行,将该渲染的元素渲染页面上。这样造成2次渲染,但是,监听scroll函数里拿不到props.children,因为这个children传过来的值并不是初始就可以取到。而children改变后,scroll的props.children还是最早的那个值。 我试了useState把值存进去,scroll去useState里拿props.children,结果无效。 试了useState存函数,结果会造成无限递归。。 试了改函数作用域,用bind取this,搞了半天都不行。 最后突然想到,干嘛非要执着在scroll里拿children,我让它延迟渲染不就行了,结果加了setTimeout完美解决问题。 效果 我把占滚动条的div做成红色,为了方便观看,到时候把样式去了就行。可以看见,完美配合分段加载以及IntersectionObserver,这几个一起用完全不会冲突。这个案例同时用了这3个技术。
{
props.renderProduct.productList.map((item:Product,index:number)=>{
return (
<Card
hoverable
key={item.id}
cover={
{
if(ref){
io!.observe(ref)
}}}
>
}
>
)
})
}
传入参数都有注释
type Props = PropsWithChildren
function Virtualize(props:Props){
const [costomHeight,setCostomHeight]=useState()
const [visbleHeight,setVisibleHeight]=useState()
const [renderChildren,setRenderChildren]=useState()
const [indexNumber,setIndexNumber]=useState({
startIndex:0,
endIndex:props.insightNumber,
overScroll:0
})
const [scaleRow,setScaleRow]=useState(2)
useEffect(()=>{
if(props.children instanceof Array){
let childrenLen = props.children.length
if(childrenLen%props.columnNumber!=0){//说明最后一行没满
let remain = childrenLen%props.columnNumber
childrenLen=childrenLen+remain
}
let fullheight = childrenLen/props.columnNumber*props.itemHeight
setCostomHeight(fullheight)
let insightHeight
if(childrenLen{
let target= e.target as HTMLDivElement
let overScroll = target.scrollTop-props.startHeight//卷曲高度
let timer = overScroll/props.itemHeight*props.columnNumber
let startIndex =Math.floor(timer)//起始索引 从0开始
startIndex = startIndex<0?0:startIndex;
timer = timer%props.columnNumber/props.columnNumber//滚的每行百分比
if(timer<0)timer=0;
if(overScroll {
setIndexNumber({
startIndex,
endIndex,
overScroll
})
});
}
useEffect(()=>{
props.scaleRow?setScaleRow(props.scaleRow):null;
if(props.scrollDom)
props.scrollDom.addEventListener('scroll',throttle(scrollFunc,50))
return ()=>{
if(props.scrollDom)
props.scrollDom.removeEventListener('scroll',throttle(scrollFunc,50))
}
},[])
return (
{renderChildren}
)
}
export default Virtualize
代码里红色那个滚动条没去,可以把样式给去了。
拓展
这个组件是固定宽高的虚拟化组件,如果组件未固定宽高,那就耗费更多资源计算。如果有时间下次写着玩。