当前位置: 首页 > Web前端 > HTML5

React项目中,如何优雅的优化一个长列表

时间:2023-04-04 23:58:31 HTML5

对于一个长列表,比如1000个数组的数据结构,如果要同时渲染这1000个数据,生成对应的1000个原生dom,我们都知道原生的DOM元素很复杂,如果通过生成这么多DOM元素来实现长列表,很可能会导致网页无响应。贯穿React核心的是“虚拟dom”。我们还可以使用虚拟列表来优雅地优化长列表。原生dom呈现长列表。-tiny-virtual-list优化长列表本文原文地址发表在我的博客:https://github.com/forthealll...欢迎star和fork~本文用例代码地址是:https://github.com/forthealll...1.原生DOM渲染长列表的缺陷首先,我们在React项目中尝试在没有任何优化的情况下一次渲染1000个DOM。每个DOM都包含一个img标签。原生DOM本身就是一个非常复杂的对象,加上img标签后。渲染效果如下图所示:可以明显的看到白屏半天,因为在React中,没有做任何优化,直接渲染了这样一个包含1000张图片的dom节点,虽然React本身使用的是虚拟dom,但是,第一次渲染时,实际生成了1000个真实DOM。我们可以查看网页中真实的DOM情况,如下图:从上图可以看出,实际生成了1000个真实的DOM。一个真正的dom,进入页面后,需要渲染这1000个dom,所以白屏时间比较长。另外,在直接渲染1000个dom节点的页面触发滚动事件也会增加内存占用,如下图所示:另外,同时渲染多个dom节点也会导致以下问题:容易丢帧,因为Rendering很慢,所以无法维持浏览器的帧率。主观上会出现页面卡顿,网页失去响应,无法及时触发事件。以上效果均在PC端显示。针对特定的移动设备,没有优化直接渲染长列表带来的问题。长列表的渲染在移动端很多场景都会遇到,比如微博、动态等,对长列表进行合理的优化可以提升用户体验。2.长列表的虚拟列表优化原理长列表的优化原理很简单。基本原理可以用一句话概括:用一个数组保存所有列表元素的位置,只渲染可见区域的列表元素。当可见区域滚动时,根据滚动偏移大小和所有列表元素的位置,计算哪些元素应该在可见区域渲染。具体实现步骤如下:首先,确定长列表所在父元素的大小。父元素的大小决定了可见区域的宽度和高度。确定长列表中每个列表元素的宽度和高度。同时计算初始条件下的长度。列表的每个元素相对于父元素的位置,用一个数组保存所有列表元素的位置信息。第一次渲染时,只显示相对于父元素可视区域内的子列表元素。滚动时,父元素滚动偏移量,以重新计算应在视口内的子列表元素。这样可以保证不管怎么滚动,真正渲染出来的dom节点只是可见区域的列表元素。假设可见区域可以显示5个子列表元素,长列表一共有1000个元素,但任何时刻,实际渲染的dom节点只有5个。5.补充说明,这种情况下,父元素一般使用position:relative,子元素的定位一般使用:position:absolute或者sticky。优化虚拟列表后,1000个包含图片的dom也会显示出来,白屏时间会大大减少。具体效果如下图所示:对于没有优化的情况,优化后的虚拟列表渲染速度有明显提升。3.通过react-virtualized优化长列表社区中有很多React组件来实现虚拟列表。比较常用的是react-virtualized和react-tiny-virtual-list。前者更全面,后者更轻松。下面分别介绍这两个React组件库。一、react-virtualized介绍react-virtualized是一个优秀的实现虚拟列表的组件库。react-virtualized提供了一些实现虚拟列表、虚拟网格、虚拟表格等的基础组件,可以减少不必要的dom渲染。另外还提??供了几个高级组件,可以实现动态子元素高度,自动填充可见区域等。react-virtualized的基本组件包括:Grid:用于优化构建任意网格结构,通过在一个二维数组中,并渲染一个类似棋盘的结构。List:List是基于Grid实现的,但它是维度列表,不是网格。Table:Table也是基于Grid实现的。表格有一个固定的头部,可以垂直滚动。Masonry:它也可以水平或垂直滚动??。与Grid不同的是,每个元素都可以自定义Size,或者子元素的大小也可以是一个动态变化的Collection:类似于瀑布,也可以水平和垂直滚动。值得注意的是,这些基础组件都是从React中的PureComponent继承而来的,所以当状态发生变化时,只是进行浅层比较来决定是否重新渲染。除了这些基础组件,react-virtualized还提供了几个高级组件,如ArrowKeyStepper、AutoSizer、CellMeasurer、InfiniteLoader等,本文专门介绍常用的AutoSizer、CellMeasurer和InfiniteLoader。AutoSizer:在子元素的情况下,AutoSizer包含的子元素会根据父元素Resize的变化自动调整子元素可视区域的宽高,同时调整子元素的真实可视区域渲染的dom元素的数量。CellMeasurer:这个高阶组件可以动态改变子元素的高度,适用于事先不知道长列表中每个子元素高度的情况。InfiniteLoader:该高级组件用于Table或List的无限滚动,适用于滚动时异步请求等情况。2、react-virtualized基础组件的使用介绍下常用的基础组件Grid和List。(1)Grid的所有基础组件基本都是基于Grid的。Grid的一个简单示例如下:import{Grid}from'react-virtualized';constlist=[['Jonyyu','软件工程师','深圳','中国','广州'],['Jonyyu','软件工程师','深圳','中国','广州'],['Jonyyu','软件工程师','深圳','CHINA','广州'],['Jonyyu','软件工程师','深圳','CHINA','广州'],['Jonyyu','SoftwareEngineer','Shenzhen','CHINA','GUANGZHOU'],['Jonyyu','SoftwareEngineer','Shenzhen','CHINA','GUANGZHOU']];函数cellRenderer({columnIndex,key,rowIndex,style}){return({list[rowIndex][columnIndex]}

)}render(,rootEl);显示效果如下图所示:渲染网格也是只渲染可见区域的dom节点,一个有趣的现象是滚动条的大小。这里Grid做了细节优化。滚动条只会在滚动时显示,滚动会停止。之后滚动条会被隐藏(2)List下面我来说明一下List的使用:import{List}from'react-virtualized';从“lorem-ipsum”导入loremIpsumconstrowCount=1000;常量列表=数组(行数)。fill().map(()=>{returnloremIpsum({count:1,units:'sentences',sentenceLowerBound:3,sentenceUpperBound:3}})functionrowRenderer({key,index,isScrolling,isVisible,style})复制代码{return({list[index]}
)}exportdefaultclassTestListextendsComponent{render(){返回
}}List的使用方法也很简单,通过指定列表的总rowCount,每个rowHeight的高度和每次渲染的函数rowRenderer,就可以构建一个渲染列表。具体效果如下图所示:2.react-virtualized高阶组件的使用结合List来看react-virtualized高阶组件的使用。(1)AutoSizer首先我们来看一下使用AutoSizer的缺点。如下图,List只能指定固定大小。如果调整其父元素的大小,那么List将不会主动填充父元素的可用空间。Viewport:从上图可以看出,List不能自动填充父元素。所以这里需要用到AutoSizer。AutoSizer的使用也很简单,我们只需要基于List:classTestListextendsComponent{render(){return
{({height,width})=>(height}rowCount={list.length}rowHeight={20}rowRenderer={rowRenderer}width={width}/>)}
}}效果如下图:上面可以可见加入AutoSizer可以动态适应父元素宽高的变化。但是也有一个问题:子元素太长,换行后无法适配子元素的高度。也就是说,仅通过基础组件List不支持子元素高度动态变化的场景。(2)CellMeasurer为了解决上面子元素可以动态变化的问题,我们可以使用高阶组件CellMeasurer:constcache=newCellMeasurerCache({defaultHeight:30,fixedWidth:true});functioncellRenderer({index,key,parent,style}){console.log(index)返回({list[index]}
);}对于需要渲染的List,如下:classTestListextendsComponent{render(){return
{({height,width})=>(cache.rowHeight}deferredMeasurementCache={cache}rowRenderer={cellRenderer}width={宽度}/>)}
}}最终结果如下:从上图我们可以看出,子列表元素的高度是可以动态改变的,子列表元素的动态高度-元素可以通过CellMeasurer来实现(3)InfiniteLoader最后我们来考虑一下这个无限滚动的场景。在很多情况下,我们可能需要页面加载,这是一种常见的可见区域无限滚动场景。react-virtualized提供了一个高阶组件InfiniteLoader用于无限滚动。InfiniteLoader的使用非常简单,只要按下文档,在家就是分页进入下一页。滚动分页调用的函数是:functionloadMoreRows({startIndex,stopIndex}){returnnewPromise(function(resolve,reject){resolve()}).then(function(){//模拟ajax请求lettemList=Array(10).fill(1).map(()=>{returnloremIpsum({count:1,units:'sentences',sentenceLowerBound:3,sentenceUpperBound:3})})list=list.concat(temList)})}最终效果如下:看起来和基本组件List一样,唯一不同的是滚动时会自动执行loadMoreRows函数来更新列表(四)总结通过基本组件Grid、List而高层组件AutoSizer、CellMeasurer和InfiniteLoader,可以构建更复杂的场景,但是有一个缺陷,就是CellMeasurer在一定程度上支持动态子元素的高度变化,实际上是一种估计。边界条件较多,无法适应动态元素的场景,尤其是文本节点较多引起的高度变化。但是图像节点的动态高度支持没有太大问题。比如CellMeasurer不能支持文本动态高度的边界情况:从上图可以看出,在缓慢收缩的过程中,如果收缩过小,子元素的高度将不会动态变化放大,文本会出现重叠。4、通过react-tiny-virtual-list优化长列表react-tiny-virtual-list是一个比较轻量级的组件,实现了虚拟列表,区别于react-virtualized,支持网格、表格等渲染优化。react-tiny-virtual-list只支持列表,简单易用,源码只有700多行。使用极其简单:从'react-tiny-virtual-list'导入VirtualList;常量数据=['A','B','C','D','E','F','A','B','C','D','E','F','A','B','C','D','E','F','A','B','C','D','E','F'];exportdefaultclassTinyVirtualextendsComponent{render(){return//style属性包含item的绝对位置Letter:{data[index]},Row:#{index}}/>}}最后的渲染结果也是类似的,同样可以支持无限滚动等等。但是react-tiny-virtual-list有一个致命的缺点:它根本不支持子元素的动态高度或宽度。5.总结本文介绍了虚拟列表优化的原理,以及可以优化虚拟列表的常用React组件库。下一篇我们会详细介绍react-tiny-virtual-list和react-virtualized的源码,敬请期待。