https://github.com/bvaughn/react-virtualized
react-virtualized 是一个以高效渲染大型列表和表格数据的响应式组件
典型开发问题
如果所示, 有教室 1/2/3, 每间教室下有 1000 + 个学生
学生组件为:
- 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 本身不带这个问题的解决方案, 不过通过 https://github.com/nkbt/react-height 或者 https://github.com/bvaughn/react-virtualized/issues/610 中提到的通过计算回调的方法解决, 以使用 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 官方有提供 https://github.com/bvaughn/react-virtualized/blob/master/docs/InfiniteLoader.md , 写法同官方!
如果抛开这个经典案例, 开发的是聊天框呢?
聊天框是倒序显示, 首次加载到数据的时候, 滚动条的位置应该位于最底部, react-virtualized 中的 List 组件暴露了 scrollToRow(index) 方法给我们去实现, Student 高度不一致时直接使用有一个小问题 https://github.com/bvaughn/react-virtualized/issues/995 , 就是不能一次性滚动到底部, 暂时性的解决方法是:
- 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 方法获取滚动数据并执行相关操作, 需要注意的是, 上一页数据返回时, 应该把 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,
- heights
- })
- })
- }
react-virtualized 还有很多有趣功能, 它本身的实现也很有参考价值! 可以到 react-virtualized github https://github.com/bvaughn/react-virtualized 逛一圈
来源: https://juejin.im/post/5af03345f265da0b7964cf50