githubreact
react-virtualized是一個以高效渲染大型列表和表格數據的響應式組件git
學生組件爲: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生態中, 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逛一圈