对于较长的列表,好比1000个数组的数据结构,若是想要同时渲染这1000个数据,生成相应的1000个原生dom,咱们知道原生的dom元素是很复杂的,若是长列表经过生成如此多的dom元素来实现,极可能使网页失去响应。react
贯穿React核心的就是"virtual dom",咱们一样的能够经过用虚拟列表的方式来优雅的优化长列表git
- 原生dom渲染长列表的缺陷
- 虚拟列表优化长列表的原理
- 经过react-virtualized来优化长列表
- 经过react-tiny-virtual-list来优化长列表
本文的原文地址发布在个人博客中:github
欢迎star和fork~数组
本文的用例的代码地址为:浏览器
github.com/fortheallli…markdown
首先咱们尝试在React项目中,未作任何优化一次性渲染1000个dom,每一个dom包含一个img标签,原生dom自己是很复杂的对象,加上img标签后。渲染的效果以下图所示:数据结构
从上图咱们能够看出,是确确实实的生成了1000个真实的dom,进入页面后,须要渲染这1000个dom,所以白屏的时间很长。dom
此外,在直接渲染1000个dom节点的页面,触发滚动事件,也会使得内存用量增长,具体能够以下图所示:异步
此外同时渲染不少dom节点,也会形成一下几个问题:
容易失帧,由于渲染很慢,因此没法维持浏览器的帧率,主观上会显得页面卡顿
网页失去响应,事件等没法及时被触发
上述的效果都是在PC端展现的,对于特定的移动设备,直接无优化的渲染长列表所形成的问题会更加的放大。长列表的渲染在移动端的不少场景会遇到,好比微博,feeds流中等等。合理的优化长列表,能够提高用户体验。
优化长列表的原理很简单,基本原理能够一句话归纳:
用数组保存全部列表元素的位置,只渲染可视区内的列表元素,当可视区滚动时,根据滚动的offset大小以及全部列表元素的位置,计算在可视区应该渲染哪些元素。
具体实现步骤以下所示:
经过虚拟列表优化后,一样的显示1000个包含图片的dom,白屏时间会大大的减小。具体效果以下图所示:
对于比无优化的状况,优化后的虚拟列表渲染速度提高很明显。
社区实现虚拟列表的React组件不少,较为经常使用的是react-virtualized和react-tiny-virtual-list,前一个较为全面,第二个较为轻量。接下来会分别来介绍这俩个React组件库。
react-virtualized是一个实现虚拟列表较为优秀的组件库,react-virtualized提供了一些基础组件用于实现虚拟列表,虚拟网格,虚拟表格等等,它们均可以减少没必要要的dom渲染。此外还提供了几个高阶组件,能够实现动态子元素高度,以及自动填充可视区等等。
react-virtualized的基础组件包含:
值得注意的是这些基础组件都是继承于React中的PureComponent,所以当state变化的时候,只会作一个浅比较来肯定从新渲染与否 。
除了这几个基础组件外,react-virtualized还提供了几个高阶组件,好比ArrowKeyStepper 、AutoSizer、CellMeasurer、InfiniteLoader等,本文具体介绍经常使用的AutoSizer、CellMeasurer和InfiniteLoader。
下面咱们来介绍一下经常使用的基础组件Grid、List。
全部基础组件基本上都是基于Grid构成的,一个简单的Grid的例子以下:
import { Grid } from 'react-virtualized';
const list = [
['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU']
];
function cellRenderer ({ columnIndex, key, rowIndex, style }) {
return (
<div
key={key}
style={style}
>
{list[rowIndex][columnIndex]}
</div>
)
}
render(
<Grid
cellRenderer={cellRenderer}
columnCount={list[0].length}
columnWidth={100}
height={300}
rowCount={list.length}
rowHeight={80}
width={300}
/>,
rootEl
);
复制代码
显示的效果以下图所示:
渲染网格也是只渲染可视区的dom节点,有个有趣的现象是滚动条的大小,这里Grid作了一个细节优化,只有滚动的时候才会显示滚动条,中止滚动后会隐藏滚动条。
接着来举例说明一下List的使用:
import { List } from 'react-virtualized';
import loremIpsum from "lorem-ipsum"
const rowCount = 1000;
const list = Array(rowCount).fill().map(()=>{
return loremIpsum({
count: 1,
units: 'sentences',
sentenceLowerBound: 3,
sentenceUpperBound: 3
}
})
function rowRenderer ({
key,
index,
isScrolling,
isVisible,
style
}) {
return (
<div
key={key}
style={style}
>
{list[index]}
</div>
)
}
export default class TestList extends Component{
render(){
return <div style={{height:"300px",width:"200px"}}>
<List
width={300}
height={300}
rowCount={list.length}
rowHeight={20}
rowRenderer={rowRenderer}
/>
</div>
}
}
复制代码
List的使用方法也是极简,指定列表总条数rowCount,每一条的高度rowHeight以及每次渲染的函数rowRenderer,就能够构建一个渲染列表。具体的效果以下图所示:
结合List来看看react-virtualized高阶组件的使用。
首先来看使用不使用AutoSizer的缺点,以下图所示,List只能指定固定的大小,若是其所在的父元素的大小resize了,那么List是不会主动填满父元素的可视区的:
从上图能够看出来,List是没法自动填充父元素的。所以咱们这里须要使用AutoSizer。AutoSizer的使用也很简单,咱们只须要在List的基础上:
class TestList extends Component{
render(){
return <div>
<AutoSizer>
{({ height, width }) => (
<List
height={height}
rowCount={list.length}
rowHeight={20}
rowRenderer={rowRenderer}
width={width}
/>
)}
</AutoSizer>
</div>
}
}
复制代码
效果以下图所示:
上述能够看出来增长了AutoSizer能够动态的适应父元素宽度和高度的变化。
可是也存在一个问题:
子元素太长,换行后改变了子元素的高度后没法子适应,也就是说仅仅经过基础的组件List是不支持子元素的高度动态改变的场景。
为了解决上述的子元素能够动态变化的问题,咱们能够利用高阶组件CellMeasurer:
import { List,AutoSizer,CellMeasurer, CellMeasurerCache} from 'react-virtualized';
const cache = new CellMeasurerCache({ defaultHeight: 30,fixedWidth: true});
function cellRenderer ({ index, key, parent, style }) {
console.log(index)
return (
<CellMeasurer
cache={cache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}
>
<div
style={style}
>
{list[index]}
</div>
</CellMeasurer>
);
}
复制代码
对于须要渲染的List,以下所示:
class TestList extends Component{
render(){
return <div>
<AutoSizer>
{({ height, width }) => (
<List
height={height}
rowCount={list.length}
rowHeight={cache.rowHeight}
deferredMeasurementCache={cache}
rowRenderer={cellRenderer}
width={width}
/>
)}
</AutoSizer>
</div>
}
}
复制代码
最后的结果以下所示:
上图咱们看出来,子列表元素的高度能够动态变化,经过CellMeasurer能够实现子元素的动态高度。
最后咱们来考虑这种无限滚动的场景,不少状况下咱们可能须要分页加载,就是常见的在可视区内无限滚动的场景。react-virtualized提供了一个高阶组件InfiniteLoader用于实现无限滚动。
InfiniteLoader的使用很简单,只要按着文档来便可,就是分页的去在家下一页,滚动分页所调用的函数为:
function loadMoreRows ({ startIndex, stopIndex }) {
return new Promise(function(resolve,reject){
resolve()
}).then(function(){
//模拟ajax请求
let temList = Array(10).fill(1).map(()=>{
return loremIpsum({
count: 1,
units: 'sentences',
sentenceLowerBound:3,
sentenceUpperBound:3
})
})
list = list.concat(temList)
})
}
复制代码
最后的效果以下:
看起来跟基础组件List同样,其实惟一的区别就是会在滚动的时候自动执行loadMoreRows函数去更新list
经过基础组件Grid、List以及高阶组件AutoSizer、CellMeasurer和InfiniteLoader,已经能够构建出比较复杂的场景,可是有一个缺陷,就是CellMeasurer虽然说必定程度上支持动态子元素的高度的变化,实际上是一种估算,存在不少边界状况,没法适应于动态元素的场景,特别是文本节点较多致使的高度变化。可是对于图片节点的动态高度支持没有很大的问题。
举例一种边界状况,CellMeasurer没法支持文本动态高度的状况:
从上图能够看到,慢慢缩小的过程当中,若是缩的过小,并无动态的撑大子元素的高度,出现了文字的重叠。
react-tiny-virtual-list是一个较为轻量的实现虚拟列表的组件,不一样于react-virtualized支持网格以及表格等渲染优化。 react-tiny-virtual-list只支持列表,使用方便,其源码也只有700多行。
使用极其简单:
import VirtualList from 'react-tiny-virtual-list';
const data = ['A', 'B', 'C', 'D', 'E', 'F','A', 'B', 'C',
'D', 'E', 'F','A', 'B', 'C', 'D', 'E', 'F',
'A', 'B', 'C', 'D', 'E', 'F'];
export default class TinyVirtual extends Component {
render(){
return <VirtualList
width='100%'
height={200}
itemCount={data.length}
itemSize={50} // Also supports variable heights (array or function getter)
renderItem={({index, style}) =>
<div key={index} style={style}>
// The style property contains the item's absolute position Letter: {data[index]}, Row: #{index}
</div>
}
/>
}
}
复制代码
最后的渲染结果也是类似的,也能够支持无限滚动等等。
可是react-tiny-virtual-list有一个致命的缺点:
彻底不支持子元素的动态高度或者宽度
本文介绍了虚拟列表的优化的原理,以及经常使用的React能够优化虚拟列表的组件库。在接下来的文章中,会具体的介绍react-tiny-virtual-list和react-virtualized的源码,敬请期待。