参考一
引入
有教室1/2/3, 每间教室下有1000+个学生.
学生组件为:
1 2 3
| function Student({student}) { return <div>{student.name}</div> }
|
如果我们直接把整个列表渲染出来, 仅仅学生列表就会生成1000+个div标签.
往往, 我们的学生组件都会是:
1 2 3 4 5 6 7 8 9
| function Student({student, ...rest}) { return ( <div> ... <div>{student.name} ....</div> ... </div> ) }
|
这个时候的DOM数量就会变得难以想象.
我们都知道, DOM结构如果过大, 网页就会出现用户操作体验上的问题, 比如滚动, 点击等常用操作. 同时, 对react的虚拟DOM计算以及虚拟DOM反映到真实DOM的压力也会很大. 当用户点击切换教室时, 就会出现秒级的卡顿.
使用react-virtualized优化
解决以上问题的核心思想就是: 只加载可见区域的组件
- react-virtualized将我们的滚动场景区分为
viewport内的局部滚动
, 和基于viewport的滚动
,
- 前者相当于在页面中开辟了一个独立的滚动区域,属于内部滚动, 这跟和iscroll的滚动很类似,
- 而后者则把滚动作为了window滚动的一部分(对于移动端而言,这种更为常见).
- 基于此计算出当前所需要显示的组件.
具体实现
学生组件修改为:
1 2 3 4 5 6 7 8 9
| function Student({student, style, ...rest}) { return ( <div style={style}> ... <div>{student.name} ....</div> ... </div> ) }
|
学生列表组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import React from 'react' import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer' import { List as VList } from 'react-virtualized/dist/commonjs/List'
class StudentList extends React.Component { constructor(props) { super(props) this.state = { list: [] } } getList = () => { api.getList.then(res => { this.setState({ list: res }) }) } componentDidMount() { this.getList() } render() { const { list } = this.state const renderItem = ({ index, key, style }) => { return <Student key={key} student={list[index]} style{style} /> } return ( <div style={{height: 1000}}> <AutoSizer> {({ width, height }) => ( <VList width={width} height={height} overscanRowCount={10} rowCount={list.length} rowHeight={100} rowRenderer={renderItem} /> )} </AutoSizer> </div> ) } }
|
(外层div样式中的高度不是必须的, 比如你的网页是flex布局, 你可以用flex: 1来让react-virtualized计算出这个高度)
这个时候, 如果每个Student的高度相同的话, 问题基本上就解决啦!
可是, 问题又来了, 有时候我们的Student会是不确定高度的, 可以有两种方法解决问题, 推荐react-virtualized的CellMeasurer组件解决方案
方法二
学生列表组件修改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| import React from 'react' import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer' import { List as VList } from 'react-virtualized/dist/commonjs/List' import { CellMeasurerCache, CellMeasurer } from 'react-virtualized/dist/commonjs/CellMeasurer'
class StudentList extends React.Component { constructor(props) { super(props) this.state = { list: [] } } measureCache = new CellMeasurerCache({ fixedWidth: true, minHeight: 58 }) getList = () => { api.getList.then(res => { this.setState({ list: res }) }) } componentDidMount() { this.getList() } render() { const { list } = this.state const renderItem = ({ index, key, parent, style }) => { return ( <CellMeasurer cache={this.measureCache} columnIndex={0} key={key} parent={parent} rowIndex={index}> <Student key={key} student={list[index]} /> </CellMeasurer> ) } return ( <div style={{height: 1000}}> <AutoSizer> {({ width, height }) => ( <VList ref={ref => this.VList = ref} width={width} height={height} overscanRowCount={10} rowCount={list.length} rowHeight={this.getRowHeight} rowRenderer={renderItem} deferredMeasurementCache={this.measureCache} rowHeight={this.measureCache.rowHeight} /> )} </AutoSizer> </div> ) } }
|
虚拟列表优化长列表的原理
- 用数组保存所有列表元素的位置,只渲染可视区内的列表元素,当可视区滚动时,根据滚动的offset大小以及所有列表元素的位置,计算在可视区应该渲染哪些元素。
具体实现步骤如下所示:
- 首先确定长列表所在父元素的大小,父元素的大小决定了可视区的宽和高
- 确定长列表每一个列表元素的宽和高,同时初始的条件下计算好长列表每一个元素相对于父元素的位置,并用一个数组来保存所有列表元素的位置信息
- 首次渲染时,只展示相对于父元素可视区内的子列表元素,在滚动时,根据父元素的滚动的offset重新计算应该在可视区内的子列表元素。这样保证了无论如何滚动,真实渲染出的dom节点只有可视区内的列表元素。
- 假设可视区内能展示5个子列表元素,及时长列表总共有1000个元素,但是每时每刻,真实渲染出来的dom节点只有5个。
- 补充说明,这种情况下,父元素一般使用position:relative,子元素的定位一般使用:position:absolute或sticky
参考二
react-virtualized简介
- react-virtualized是一个以高效渲染大型列表和表格数据的响应式组件
- react-virtualized是一个实现虚拟列表较为优秀的组件库,react-virtualized提供了一些基础组件用于实现虚拟列表,虚拟网格,虚拟表格等等,它们都可以减小不必要的dom渲染。此外还提供了几个高阶组件,可以实现动态子元素高度,以及自动填充可视区等等。
react-virtualized的基础组件包含:
- Grid:用于优化构建任意网状的结构,传入一个二维的数组,渲染出类似棋盘的结构。
- List:List是基于Grid来实现的,但是是一个维的列表,而不是网状。
- Table:Table也是基于Grid来实现,表格具有固定的头部,并且可以在垂直方向上滚动
- Masonry:同样可以在水平方向,也可以在垂直方向滚动,不同于Grid的是可以自定义每个元素的大小,或者子元素的大小也可以是动态变化的
- Collection:类似于瀑布流的形式,同样可以水平和垂直方向滚动。
值得注意的是这些基础组件都是继承于React中的PureComponent,因此当state变化的时候,只会做一个浅比较来确定重新渲染与否。
除了这几个基础组件外,react-virtualized还提供了几个高阶组件,比如ArrowKeyStepper、AutoSizer、CellMeasurer、InfiniteLoader等,本文具体介绍常用的AutoSizer、CellMeasurer和InfiniteLoader。
- AutoSizer:用于一个子元素的情况,通过AutoSizer包含的子元素会根据父元素Resize的变化,自动调节该子元素的可视区的宽度和高度,同时调节的还有该子元素可视区真实渲染的dom元素的数目。
- CellMeasurer:这个高阶组件可以动态的改变子元素的高度,适用于提前不知道长列表中每一个子元素高度的情况。
- InfiniteLoader:这个高阶组件用于Table或者List的无限滚动,适用于滚动时异步请求等情况
react-virtualized基础组件的使用
Grid
- 所有基础组件基本上都是基于Grid构成的,
- 一个简单的Grid的例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| 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} rowCount={list.length} columnWidth={100} rowHeight={80} height={300} width={300} />, rootEl );
|
- 渲染网格也是只渲染可视区的dom节点,
- 有个有趣的现象是滚动条的大小,这里Grid做了一个细节优化,只有滚动的时候才会显示滚动条,停止滚动后会隐藏滚动条。
List
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| 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,就可以构建一个渲染列表。
react-virtualized高阶组件的使用
AutoSizer
- 首先来看使用不使用AutoSizer的缺点,List只能指定固定的大小,如果其所在的父元素的大小resize了,那么List是不会主动填满父元素的可视区的:
- List是无法自动填充父元素的。因此我们这里需要使用AutoSizer。AutoSizer的使用也很简单,我们只需要在List的基础上:
- 增加了AutoSizer可以动态的适应父元素宽度和高度的变化。
CellMeasurer
- 上面代码也有问题,子元素太长,换行后改变了子元素的高度后无法子适应,也就是说仅仅通过基础的组件List是不支持子元素的高度动态改变的场景。
- 为了解决上述的子元素可以动态变化的问题,我们可以利用高阶组件CellMeasurer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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可以实现子元素的动态高度。
InfiniteLoader
最后我们来考虑这种无限滚动的场景,很多情况下我们可能需要分页加载,就是常见的在可视区内无限滚动的场景。react-virtualized提供了一个高阶组件InfiniteLoader用于实现无限滚动。
InfiniteLoader的使用很简单,只要按着文档来即可,就是分页的去在家下一页,滚动分页所调用的函数为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function loadMoreRows ({ startIndex, stopIndex }) { return new Promise(function(resolve,reject){ resolve() }).then(function(){ let temList = Array(10).fill(1).map(()=>{ return loremIpsum({ count: 1, units: 'sentences', sentenceLowerBound:3, sentenceUpperBound:3 }) }) list = list.concat(temList) }) }
|
看起来跟基础组件List一样,其实唯一的区别就是会在滚动的时候自动执行loadMoreRows函数去更新list
参考三
主要功能可以从两方面概括:
- 布局
- Collection
- Grid
- List
- Masonry(堆砌)
- Table
- 可组装功能
- Arrow Key Stepper,单元格的方向键导航
- AutoSizer,自动管理列表/表格的宽度和高度
- CellMeasurer,自动计算单元格的高度,适用于动态高度的数据行
- ColumnSizer,设定列宽的宽高管理
- InfiniteLoader,支持分页预读取的无限滚动
- MultiGrid, 类似冻结行列的组合Grid
- ScrollSync,用于同步多个Grid之间的滚动的工具组件
- WindowScroller,用于把滚动事件绑定在列表之上的容器(div或window)
这里以搜索界面的代码为例,这是一个包含了分页数据、外部容器滚动、动态高度列表。在这个例子里,我们用到了以下这些类(按嵌套的层级关系列出):
- InfiniteLoader,用于分页取结果
- WindowScroller,用于将滚动事件绑定在外部容器上
- CellMeasurerCache,用于计算和缓存不同的单元格的高度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| renderSearchResult() { const { recordPerPage, onFetchMore, refLayoutMain } = this.props; const remoteRowCount = this.state.result.total; const cache = this.cellMeasurerCache;
if (!refLayoutMain) { return null; }
return ( <InfiniteLoader isRowLoaded={this.isResultRowLoaded} loadMoreRows={onFetchMore} rowCount={remoteRowCount} threshold={recordPerPage - PAGE_PREFETCH_THRESHOLD} > {({ onRowsRendered, registerChild }) => { // logger.debug('render window scroller with ref:', refLayoutMain); const listMinHeight = 40; const defaultPanelWidth = 896; const mockOnRowsRendered = (...args) => { const ret = onRowsRendered(...args); // logger.debug('onRowsRendered:', ...args, 'return value:', ret); return ret; };
return (<WindowScroller scrollElement={refLayoutMain} > {({ height, isScrolling, onChildScroll, scrollTop }) => { // logger.debug( // 'Window scroller render with height:', height, 'container:', refLayoutMain); return ( <List ref={(node) => { this.refResultList = node; registerChild(node); }} onRowsRendered={mockOnRowsRendered} className="setting-main-custom-card search-result-scroller" autoHeight height={height || listMinHeight} width={this.panelWidth || defaultPanelWidth} isScrolling={isScrolling} onScroll={onChildScroll} scrollTop={scrollTop} deferredMeasurementCache={cache} rowHeight={cache.rowHeight} rowCount={remoteRowCount} rowRenderer={this.renderListCell}
/> ); }} </WindowScroller>); }}
</InfiniteLoader>); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| renderListCell = ({ index, key, parent, style }) => { const resultList = this.state.result.list; const data = (index === resultList.length && this.props.searchResult.has_next) ? loadingResultData.list[0] : resultList[index]; const cache = this.cellMeasurerCache;
return ( <CellMeasurer cache={cache} columnIndex={0} key={key} parent={parent} rowIndex={index} > <SearchResultItemRenderer data={data} style={style} /> </CellMeasurer> ); }
|