前言

  • 上次那篇写的虚拟滚动后来使用发现在某些情况并不是特别好用,并且只支持固定高度。我看了下umihook的虚拟滚动,发现也不是很好用,它支持手动设定每个元素高度,但也不能支持不定高度,而且限定更多了,比如不能在同一个滚动dom下绑定多个虚拟滚动,对跨组件调用不太友好,甚至第一次出现可能不会显示,需要划一下或者使用scrollto才会出现。
  • 不过umihook的有些设定还是可以对我有些启发的。加上以前我就知道有种方法可以不用知道高度进行虚拟滚动,所以就写写这玩意。

思路

  • 我以前写的虚拟滚动,有些参数可以不需要的,有些参数可以优化下,但是原理还是一样。
  • 能滚动的dom是必拿的,渲染的list我们可以包裹一层,从而相减拿到两者之差的起始位置,省去人为进行计算。
  • 还是先做定高的,定高做完再做不定高。

步骤

  • 首先还是要拿到滚动dom。通过scrolldom传来,然后还有个children的wrapper,这里直接做到组件里。
  • 拿到dom后获取其各参数,存入state,为啥不用usememo?因为memo存进去没法刷新,我存进去需要刷新下。
  • 有了参数,我就可以计算起始高度了:
 const [scrollDomParams, setScrollDomParams] = useState({width: 0,height: 0,top: 0,left: 0,});useEffect(() => {if (props.scrollDom.current) {const rect = props.scrollDom.current.getBoundingClientRect();setScrollDomParams({width: rect.width,height: rect.height,left: rect.left,top: rect.top,});}}, [props.scrollDom]);const [childrenWrapParams, setChildrenWrapParams] = useState({width: 0,height: 0,top: 0,left: 0,});const ref = useRef<HTMLDivElement>(null);useEffect(() => {if (ref.current) {const rect = ref.current.getBoundingClientRect();setChildrenWrapParams({width: rect.width,height: rect.height,left: rect.left,top: rect.top,});}}, []);const wrapperToScrollDomDistance = useMemo(() => {return childrenWrapParams.top - scrollDomParams.top;}, [childrenWrapParams.top, scrollDomParams.top]);
  • 下面需要制作模拟滚动条,由于我们要做定高的,所以需要传递每个高度进来,然后根据length计算总共高度,这样模拟滚动条高度还要减去开始那个高度即是总共高度。
 const mockHeight = useMemo(() => {return arrayResolve<number>(props.children,(val: any[]) =>val.length * props.itemHeight - wrapperToScrollDomDistance,() => 0);}, [props.children, props.itemHeight, wrapperToScrollDomDistance]);
  • 有了模拟滚动条,后面则是制作虚拟滚动渲染了,我们需要将拿到的孩子从手里过一遍,得到需要渲染的孩子。
  • 需要设定个渲染元素个数,渲染的多,往下滚动时不容易看见空白。
  • 由于这个改变元素渲染需要刷新,所以这个我也做到state里。
function arrayResolve<R>(value: any,isArrayFunc: Function,notArrayFunc: Function
): R {if (Array.isArray(value)) {return isArrayFunc(value);} else {console.error("you must pass array children ");return notArrayFunc();}
}const [renderChildren, setRenderChildren] = useState(//一开始,需要返回对应截取的元素() => {return arrayResolve<ReactChildren>(props.children,(val: any[]) => val.slice(0, props.renderNumber),() => null);});
  • 下面需要绑定滚动监听,自然是绑到scrolldom上,当滚动时计算渲染位置。
  • 在每次滚动时,scrolltop会发生改变,比如当我滚到100时,我前面应该减少100除每个元素高个元素,后面应该增加同样的元素进行渲染。
  • 所以要在监听函数中计算scroll的值除每个元素的高,再从头和尾加对应元素即可,另一方面我们还需要控制视口的移动,由于有初始高度,所以视口在滚过初始高度后才可以移动。这里调整translateY即可。
useEffect(() => {let fn: (e: Event) => void;if (props.scrollDom.current) {fn = (e: Event) => {const target = e.target as HTMLDivElement;const scroll = target.scrollTop;const iNumber = Math.floor(scroll / props.itemHeight);let Y = scroll - wrapperToScrollDomDistance;if (Y < 0) {Y = 0;//最后的scroll 需要减去一屏幕高度} else if (Y >= mockHeight - scrollDomParams.height) {Y = mockHeight - scrollDomParams.height;}unstable_batchedUpdates(() => {setRenderChildren(arrayResolve<ReactChildren>(props.children,(val: any[]) =>val.slice(0 + iNumber,props.renderNumber + iNumber),() => null));setViewPortY(Y);});};props.scrollDom.current.addEventListener("scroll", fn);}return () => {if (props.scrollDom.current) {//解绑非常重要,否则渲再次出现渲染会出严重问题props.scrollDom.current.removeEventListener("scroll", fn);}};}, [mockHeight,props.children,props.itemHeight,props.renderNumber,props.scrollDom,scrollDomParams.height,wrapperToScrollDomDistance,]);
  • 这样一个定高的虚拟滚动就做好了,是不是很简单呢?
  • 下面需要制作不定高的虚拟滚动,不定高的话制作起来就比较困难,我觉得做动态赋给高度的意义不大,既然用了虚拟滚动,那么牺牲点性能傻瓜式代入自动算高度才是最舒服的组件。
  • 不定高难度就在于各个元素高度不定,这样滚动条滚到一定地步到底有没有就不知道,所以在什么都不知道的情况下,我们需要让用户给每个元素的参考高度,便于去估算大致的滚动条高度,渲染后再动态调整剩余滚动条高度。
  • 其他选项则与定高相同。为了方便,我新建个文件进行制作。
  • 首要则是制作个可以动态拿到渲染的dom高度并且执行动态修正模拟高度的函数。比如用户传来每个元素大概20px高,有100个元素,那么估算高度为100*20,2000px,我渲染出第一个元素到页面上发现它有10px高,那么我就得修正2000px,原来预估20px,那么就用2000-20+10=1900px,反之,如果我第二个元素到页面上有30px,那么就是1900px-20+30=2000px。通过不断修正滚动条来完成。
  • 下面我会使用个对象来做个缓存,先预设用户给的高度,再进行计算:
//为每个元素建立高度const cache = useMemo(() => {return arrayResolve<Record<number, number>>(props.children,(val: any[]) => {return val.reduce((prev, next, index) => {prev[index] = props.referItemHeight;return prev;}, {});},() => {});}, [props.children, props.referItemHeight]);const mockHeight = useMemo(() => {return Object.values(cache).reduce((prev, next) => prev + next, 0);}, [cache]);
  • 由于我们要动态调整mock滚动条,所以需要把mockheight变为state:
  const [mockHeight,setMockHeight]=useState(()=>{return Object.values(cache).reduce((prev, next) => prev + next, 0);})useEffect(()=>{setMockHeight(Object.values(cache).reduce((prev, next) => prev + next, 0))},[cache])
  • 下面会比较麻烦,我们需要渲染出元素然后获取其高度,我们需要加快获取元素进度就要用uselayouteffect。
  • 同时,我们需要让其注册到ref上才可以获取。
  • 这里就还需要考虑下内存问题,估计这也是umihook没做自动获取高度的原因。但是我们可以牺牲性能来获取更好的体验。
 const refData: Record<number, HTMLDivElement> = useMemo(() => {return {};}, []);const cloneChildren = useMemo(() => {return arrayResolve<ReactElement[]>(props.children,(val: ReactElement[]) => {return val.map((v, i) => {const oprops = v.props;return React.cloneElement(v, {...oprops,ref: (node:HTMLDivElement) => {refData[i] = node;},});});},() => []);}, [props.children, refData]);
  • 后续操作就会换成cloneChildren操作。
  • 初次渲染,立即调整cache中的高度:
//初次返回,我们进行修正cache //初次渲染 0- props.renderNumberuseLayoutEffect(() => {if (//如果0存在,说明已经显示了,refData[0]) {//map rendernumbernew Array(props.renderNumber).fill(1).forEach((x, y) => {const height =refData[y].getBoundingClientRect().height || cache[y];cache[y] = height;});}// eslint-disable-next-line react-hooks/exhaustive-deps}, []);
  • 后面修改监听scroll逻辑。
  • 这里我们不能对元素进行增减固定值的操作,因为这样会导致元素明明有50px高,结果滚动了20px就滚过了50px导致最终计算错误。所以这里需要利用缓存的高度计算滚到的第一个位置,再从第一个位置加上用户传的,即为应该渲染在页面的元素。
  • 每次进行滚动,我们需要动态修正cache的高度,同时删除ref中减少的dom(防止内存过大)。当一轮滚动彻底结束后,缓存全部都有,dom也都删光,回滚时,记录的长度如果大于已更新长度,则不会触发后续更新缓存操作。
 const current = useMemo(() => {return {//以start为界。每次删除前面的,加入后面的,并且修正cachestart: props.renderNumber,};}, [props.renderNumber]);const maxY = useMemo(() => {//最大值等于mock高减去一屏幕高度return mockHeight - scrollDomParams.height;}, [mockHeight, scrollDomParams.height]);useEffect(() => {let fn: (e: Event) => void;let timer: number;if (props.scrollDom.current) {fn = (e: Event) => {const target = e.target as HTMLDivElement;const scroll = target.scrollTop;//根据scroll的高度判断滚到第几个位置let sum = 0;let sindex = 0;Object.values(cache).some((v, i) => {sum = sum + v;if (sum > scroll) {sindex = i;return true;}return false;});const remain =props.renderNumber + sindex > cloneChildren.length? cloneChildren.length: props.renderNumber + sindex;const start = current.start;if (start < remain && start < cloneChildren.length) {timer = window.setTimeout(() => {for (let i = start; i < remain; i++) {if (refData[i]) {const height =refData[i].getBoundingClientRect().height ||cache[i];cache[i] = height;}}setMockHeight(Object.values(cache).reduce((prev, next) => prev + next,0));current.start = remain;//删除start之前的domfor (let i = 0; i < start; i++) {if (refData[i]) {delete refData[i];}}});}let Y = scroll - wrapperToScrollDomDistance;if (Y < 0) {Y = 0;//最后的scroll 需要减去一屏幕高度} else if (Y >= maxY) {Y = maxY;}unstable_batchedUpdates(() => {setRenderChildren(cloneChildren.slice(0 + sindex, remain));setViewPortY(Y);});};props.scrollDom.current.addEventListener("scroll", fn);}return () => {if (props.scrollDom.current) {//解绑非常重要,否则渲再次出现渲染会出严重问题props.scrollDom.current.removeEventListener("scroll", fn);}window.clearTimeout(timer);};}, [cache,cloneChildren,current,maxY,props.referItemHeight,props.renderNumber,props.scrollDom,refData,wrapperToScrollDomDistance,]);
  • 最终,这个可以自动获取高度的虚拟滚动组件就做好了!
  • 0.3版本进行修复bug,上面做的虚拟滚动最上面元素都在第一个位置呈现,所以进行修改,往前多渲染一屏幕即可解决。
  • 可以看看组件最终效果:https://github.com/yehuozhili/yh-react-virtuallist

【React】手写虚拟滚动组件(二)可自动获取不定高度的虚拟滚动组件相关推荐

  1. React,手写简易redux(二)- By Viga

    React,手写简易redux(二) 本章节会完成一个简易的redux实现 该系列内容会逐步实现简易的redux 使用技术栈:vite+react 该系列感谢@方应杭 的react教学视频 目录 实现 ...

  2. mnist手写数字识别python_基于tensorflow的MNIST手写数字识别(二)--入门篇

    一.本文的意义 因为谷歌官方其实已经写了MNIST入门和深入两篇教程了,那我写这些文章又是为什么呢,只是抄袭?那倒并不是,更准确的说应该是笔记吧,然后用更通俗的语言来解释,并且补充更多,官方文章中没有 ...

  3. JS手写上传文件、React手写上传文件

    目录 JS手写 React上传文件 JS手写 <!DOCTYPE html> <html lang="en"><head><meta ch ...

  4. html手写vue多级选择框,vue + html 编写仿element select 多选组件

    现在做vue项目主要用的ui框架差不多都是elementui,但是每个项目设计的不同难免和element组件产生差异甚至是大不相同,有的时候差异少比如页面样式不太相同,功能使用完全一样的话,这样改改样 ...

  5. 《视觉SLAM进阶:从零开始手写VIO》(二)

    <视觉SLAM进阶:从零开始手写VIO>第二讲 1 安装im_utils 这个工具之前就使用过了,还写了博客,没想到在这里用上了,博客地址:https://blog.csdn.net/le ...

  6. 《深度学习之TensorFlow》reading notes(3)—— MNIST手写数字识别之二

    文章目录 模型保存 模型读取 测试模型 搭建测试模型 使用模型 模型可视化 本文是在上一篇文章 <深度学习之TensorFlow>reading notes(2)-- MNIST手写数字识 ...

  7. WPS小技巧:在word进行手写批注、观看版式、自动保存、检测拼写错误的单词。

    手写批注: 在审阅中可以看到一个画笔功能 点开后可以进行手写批注: 观看版式: 在视图选项卡中可以看到各种不同的版式: 选择不同,表现效果也不同: 全屏显示: 阅读模式:  写作模式:  大纲模式: ...

  8. Vue.JS实现垂直方向展开、收缩不定高度模块的JS组件

    demo地址: https://github.com/XieTongXue/how-to/tree/master/vertical-toggle 需求分析: 如图,有很多高度不固定的模块(图中只显示两 ...

  9. MNIST数据集手写数字识别(二)

    上一篇对MNIST数据集有了一些了解,数据集包含着60000张训练图片与标签值和10000张测试图片与标签值的数据集,数据集有了,现在我们来构造神经网络,预测下对这测试的10000张图片的正确识别率, ...

最新文章

  1. 2014西安 H 有向图博弈 UVALive-7042
  2. 导出mysql sql语句吗_mysql sql语句导入与导出
  3. html文件已传入服务器,把html文件上传到云服务器
  4. boost::function2用法的测试程序
  5. 零基础学Python(第五章 运算符)
  6. asp.net mvc使用mysql_ASP.NET开发实战——(八)ASP.NET MVC 与数据库之MySQL
  7. 牛客练习赛 67——ST表
  8. Unity Shader 屏幕后效果——Bloom外发光
  9. 裸奔时代,如何找回自己的隐私?
  10. SlickEdit 的宏解析设置方法
  11. python语音读取
  12. Java计算机毕业设计电脑小白网站源码+系统+数据库+lw文档
  13. 利用LFW对人脸识别模型进行精度评测
  14. navicat win32注册机下载 | 绿色版
  15. 计算机硬盘和分区是什么关系,电脑硬盘如何分区 电脑硬盘分区注意事项【详解】...
  16. 我体验了禾多科技的自动驾驶汽车,离量产不远了!
  17. python提取pdf文字,python 提取pdf文字
  18. 将文本中的各个单词的字母顺序翻转(Java)
  19. 一个能防止改名木马漏洞的无组件上传类
  20. 主流微服务配置中心对比 config,nacso和Apollo对比

热门文章

  1. cordova版本更新_如何升级cordova插件
  2. Rational Function
  3. BAI公布2021年全球创新奖最终入围名单
  4. C语言(一)认识了解C语言
  5. 微信公众号的常见问题
  6. python图片转化pdf
  7. 联想Thinkbook Ubuntu18.04 安装nvidia显卡驱动
  8. 取消珊瑚虫qq的一键锁定
  9. Vue中 引入使用 js-pinyin 实现汉字转拼音
  10. 实现类似微博视频滚动自动播放与暂停