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