前言
本章节将要介绍一下 React 对于大量数据的处理方案,对于项目中大量数据通常存在两种情况:
- 第一种就是数据可视化,比如像热力图,地图,大量的数据点位的情况。
- 第二种情况是长列表渲染。
接下来将重点围绕这两点展开讨论,通过本章节,将收获 React 应用处理大量数据的解决方案。
实践一 时间分片
时间分片主要解决,初次加载,一次性渲染大量数据造成的卡顿现象。浏览器执 js 速度要比渲染 DOM 速度快的多。,时间分片,并没有本质减少浏览器的工作量,而是把一次性任务分割开来,给用户一种流畅的体验效果。就像造一个房子,如果一口气完成,那么会把人累死,所以可以设置任务,每次完成任务一部分,这样就能有效合理地解决问题。
所以接下来实践一个时间分片的 demo ,一次性加载 20000 个元素块,元素块的位置和颜色是随机的。首先假设对 demo 不做任何优化处理。
色块组件:
/* 获取随机颜色 */
function getColor(){
const r = Math.floor(Math.random()*255);
const g = Math.floor(Math.random()*255);
const b = Math.floor(Math.random()*255);
return 'rgba('+ r +','+ g +','+ b +',0.8)';
}
/* 获取随机位置 */
function getPostion(position){
const { width , height } = position
return { left: Math.ceil( Math.random() * width ) + 'px',top: Math.ceil( Math.random() * height ) + 'px'}
}
/* 色块组件 */
function Circle({ position }){
const style = React.useMemo(()=>{ //用useMemo缓存,计算出来的随机位置和色值。
return {
background : getColor(),
...getPostion(position)
}
},[])
return <div style={style} className="circle" />
}
- 子组件接受父组件的位置范围信息。并通过 useMemo 缓存计算出来随机的颜色,位置,并绘制色块。
父组件:
class Index extends React.Component{
state={
dataList:[], // 数据源列表
renderList:[], // 渲染列表
position:{ width:0,height:0 } // 位置信息
}
box = React.createRef()
componentDidMount(){
const { offsetHeight , offsetWidth } = this.box.current
const originList = new Array(20000).fill(1)
this.setState({
position: { height:offsetHeight,width:offsetWidth },
dataList:originList,
renderList:originList,
})
}
render(){
const { renderList, position } = this.state
return <div className="bigData_index" ref={this.box} >
{
renderList.map((item,index)=><Circle position={ position } key={index} /> )
}
</div>
}
}
/* 控制展示Index */
export default ()=>{
const [show, setShow] = useState(false)
const [ btnShow, setBtnShow ] = useState(true)
const handleClick=()=>{
setBtnShow(false)
setTimeout(()=>{ setShow(true) },[])
}
return <div>
{ btnShow && <button onClick={handleClick} >show</button> }
{ show && <Index /> }
</div>
}
- 父组件在 componentDidMount 模拟数据交互,用ref获取真实的DOM元素容器的宽高,渲染列表。
效果:
可以直观看到这种方式渲染的速度特别慢,而且是一次性突然出现,体验不好,所以接下来要用时间分片做性能优化。
// TODO: 改造方案
class Index extends React.Component{
state={
dataList:[], //数据源列表
renderList:[], //渲染列表
position:{ width:0,height:0 }, // 位置信息
eachRenderNum:500, // 每次渲染数量
}
box = React.createRef()
componentDidMount(){
const { offsetHeight , offsetWidth } = this.box.current
const originList = new Array(20000).fill(1)
const times = Math.ceil(originList.length / this.state.eachRenderNum) /* 计算需要渲染此次数*/
let index = 1
this.setState({
dataList:originList,
position: { height:offsetHeight,width:offsetWidth },
},()=>{
this.toRenderList(index,times)
})
}
toRenderList=(index,times)=>{
if(index > times) return /* 如果渲染完成,那么退出 */
const { renderList } = this.state
renderList.push(this.renderNewList(index)) /* 通过缓存element把所有渲染完成的list缓存下来,下一次更新,直接跳过渲染 */
this.setState({
renderList,
})
requestIdleCallback(()=>{ /* 用 requestIdleCallback 代替 setTimeout 浏览器空闲执行下一批渲染 */
this.toRenderList(++index,times)
})
}
renderNewList(index){ /* 得到最新的渲染列表 */
const { dataList , position , eachRenderNum } = this.state
const list = dataList.slice((index-1) * eachRenderNum , index * eachRenderNum )
return <React.Fragment key={index} >
{
list.map((item,index) => <Circle key={index} position={position} />)
}
</React.Fragment>
}
render(){
return <div className="bigData_index" ref={this.box} >
{ this.state.renderList }
</div>
}
}
- 第一步:计算时间片,首先用 eachRenderNum 代表一次渲染多少个,那么除以总数据就能得到渲染多少次。
- 第二步:开始渲染数据,通过
index>times判断渲染完成,如果没有渲染完成,那么通过 requestIdleCallback 代替 setTimeout 浏览器空闲执行下一帧渲染。 - 第三步:通过 renderList 把已经渲染的 element 缓存起来,渲染控制章节讲过,这种方式可以直接跳过下一次的渲染。实际每一次渲染的数量仅仅为 demo 中设置的 500 个。
完美达到效果(这个是 gif 形式,会出现丢帧的情况,在真实场景,体验感更好):
