react-virtualized是一个以高效渲染大型列表和表格数据的响应式组件
典型开发问题
![](http://www.mk2048.com/web_upload/blog_imgs/7/https___user-gold-cdn-xitu-io_2018_5_7_1633aaa0934caa53_w-1052_h-980_f-png_s-51636.gif)
学生组件为:
function Student({student}) {
return <div>{student.name}</div>
}
如果我们直接把整个列表渲染出来, 仅仅学生列表就会生成1000 个div标签.
往往, 我们的学生组件都会是:
function Student({student, ...rest}) {
return (
<div>
...
<div>{student.name} ....</div>
...
</div>
)
}
这个时候的DOM数量就会变得难以想象.
我们都知道, DOM结构如果过大, 网页就会出现用户操作体验上的问题, 比如滚动, 点击等常用操作. 同时, 对react的虚拟DOM计算以及虚拟DOM反映到真实DOM的压力也会很大. 当用户点击切换教室时, 就会出现秒级的卡顿.
使用react-virtualized优化
在react生态中, react-virtualized作为长列表优化的存在已久, 社区一直在更新维护, 讨论不断, 同时也意味着这是一个长期存在的棘手问题! ?
解决以上问题的核心思想就是: 只加载可见区域的组件
react-virtualized将我们的滚动场景区分为了viewport内的局部滚动, 和基于viewport的滚动, 前者相当于在页面中开辟了一个独立的滚动区域,属于内部滚动, 这跟和iscroll的滚动很类似, 而后者则把滚动作为了window滚动的一部分(对于移动端而言,这种更为常见). 基于此计算出当前所需要显示的组件.
具体实现
学生组件修改为:
function Student({student, style, ...rest}) {
return (
<div style={style}>
...
<div>{student.name} ....</div>
...
</div>
)
}
学生列表组件:
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组件解决方案
方法一
学生列表组件修改为:
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>
)
}
}
方法二
通过react-height或者issue中提到的通过计算回调的方法解决, 以使用react-height为例:
学生列表组件修改为:
import React from 'react'
import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer'
import { List as VList } from 'react-virtualized/dist/commonjs/List'
import ReactHeight from 'react-height'
class StudentList extends React.Component {
constructor(props) {
super(props)
this.state = {
list: []
heights = []
}
}
getList = () => {
api.getList.then(res => {
this.setState({
list: res
})
})
}
componentDidMount() {
this.getList()
}
handleHeightReady = (height, index) => {
const heights = [...this.state.heights]
heights.push({
index,
height
})
this.setState({
heights
}, this.vList.recomputeRowHeights(index))
}
getRowHeight = ({ index }) => {
const row = this.heights.find(item => item.index === index)
return row ? row.height : 100
}
render() {
const { list } = this.state
const renderItem = ({ index, key, style }) => {
if (this.heights.find(item => item.index === index)) {
return <Student key={key} student={list[index]} style{style} />
}
return (
<div key={key} style={style}>
<ReactHeight
onHeightReady={height => {
this.handleHeightReady(height, index)
}}
>
<Student key={key} student={list[index]} />
</ReactHeight>
</div>
)
}
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}
/>
)}
</AutoSizer>
</div>
)
}
}
现在, 如果你的列表数据都是一次性获取得来的话, 基本上是解决问题了!
那如果是滚动加载呢?
react-virtualized官方有提供InfiniteLoader, 写法同官方!
如果抛开这个经典案例, 开发的是聊天框呢?
聊天框是倒序显示, 首次加载到数据的时候, 滚动条的位置应该位于最底部, react-virtualized中的List组件暴露了scrollToRow(index)方法给我们去实现, Student高度不一致时直接使用有一个小问题, 就是不能一次性滚动到底部, 暂时性的解决方法是:
scrollToRow = (): void => {
const rowIndex = this.props.list.length - 1
this.vList.scrollToRow(rowIndex)
clearTimeout(this.scrollToRowTimer)
this.scrollToRowTimer = setTimeout(() => {
if (this.vList) {
this.vList.scrollToRow(rowIndex)
}
}, 10)
}
在首次加载到数据时调用
由于InfiniteLoader并不支持倒序加载这样的需求, 只能自己通过onScroll方法获取滚动数据并执行相关操作, 需要注意的是, 上一页数据返回时, 如果使用方法一, 需要执行this.measureCache.clear/clearAll, 通知react-virtualized重新计算. 方法二则 应该把state.heights数组中的index全部加上本次数据的数量
getList = () => {
api.getList.then(res => {
const heights = [...this.state.heights]
heights.map(item => {
return {
index: item.index res.length,
height: item.height
}
})
this.setState({
list: [...res, ...this.state.list],
heights
})
})
}
react-virtualized还有很多有趣功能, 它本身的实现也很有参考价值! 可以到react-virtualized github逛一圈