react長列表優化方案: react-virtualized

githubreact

react-virtualized是一個以高效渲染大型列表和表格數據的響應式組件git

典型開發問題

若是所示, 有教室1/2/3, 每間教室下有1000+個學生

學生組件爲:github

function Student({student}) {
    return <div>{student.name}</div>
}
複製代碼

若是咱們直接把整個列表渲染出來, 僅僅學生列表就會生成1000+個div標籤.api

每每, 咱們的學生組件都會是:數組

function Student({student, ...rest}) {
    return (
        <div>
            ...
                <div>{student.name} ....</div>
            ...
        </div>
    )
}
複製代碼

這個時候的DOM數量就會變得不可思議.bash

咱們都知道, DOM結構若是過大, 網頁就會出現用戶操做體驗上的問題, 好比滾動, 點擊等經常使用操做. 同時, 對react的虛擬DOM計算以及虛擬DOM反映到真實DOM的壓力也會很大. 當用戶點擊切換教室時, 就會出現秒級的卡頓.佈局

使用react-virtualized優化

在react生態中, react-virtualized做爲長列表優化的存在已久, 社區一直在更新維護, 討論不斷, 同時也意味着這是一個長期存在的棘手問題! 😂flex

解決以上問題的核心思想就是: 只加載可見區域的組件優化

react-virtualized將咱們的滾動場景區分爲了viewport內的局部滾動, 和基於viewport的滾動, 前者至關於在頁面中開闢了一個獨立的滾動區域,屬於內部滾動, 這跟和iscroll的滾動很相似, 然後者則把滾動做爲了window滾動的一部分(對於移動端而言,這種更爲常見). 基於此計算出當前所須要顯示的組件.ui

具體實現

學生組件修改成:

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逛一圈

相關文章
相關標籤/搜索