react进阶第九讲:渲染海量数据
方式1:时间片段
将一次性任务划分成多个片段(Fragments),每次只完成一部分。
将10w个彩色点渲染到dom:
生成色块代码:
/* 获取随机颜色 */
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" />
}
CSS:
.bigData_index{
position: fixed;
left:0;
right: 0;
top:0;
bottom: 0;
}
.circle{
position: absolute;
width: 7px;
height: 7px;
border-radius: 50%;
}
.list{
list-style: none;
background-color: #fc4838;
padding: 10px 20px;
color: #fff;
height: 50px;
line-height: 50px;
box-sizing: border-box;
margin-bottom: 10px;
margin-left: 24px;
margin-right: 24px;
font-weight: bold;
border-radius:10px ;
}
.list_box{
position: fixed;
left:0;
top:60px;
overflow: scroll;
bottom:0;
right: 0;
}
未优化的代码:
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>
}
10WDOM一起渲染,界面会出现明显的卡顿。优化后代码:
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(100000).fill(1)
const times = Math.ceil(originList.length / this.state.eachRenderNum) /* 需要渲染此次数*/
let index = 1
this.setState({
position: { height:offsetHeight,width:offsetWidth },
dataList:originList
},()=>{
this.toRenderList(index,times)
})
}
toRenderList=(index,times)=> {
if(index===times) return
const { renderList } = this.state
renderList.push(this.renderNewList(index))
this.setState({
renderList,
})
setTimeout(()=>{ /* 用 requestIdleCallback 代替 setTimeout */
this.toRenderList(++index,times)
},0)
}
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)=>{
return <Circle key={index} position={position} />
})
}
</React.Fragment>
}
render(){
return <div className="bigData_index" ref={this.box} >
{ this.state.renderList }
</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>
}
重点:
- 将数据分割,形成二维数组[[1,1,...,1], [1,1,...,1], ..., [1,1,...,1]]
- 使用React.Fragment 分割成代码片段去渲染
方式二:虚拟列表
虚拟列表划分可以分为三个区域:视图区 + 缓冲区 + 虚拟区。
- 通过 useRef 获取元素,缓存变量。
- useEffect 初始化计算容器的高度。截取初始化列表长度。这里需要 div 占位,撑起滚动条。
- 通过监听滚动容器的 onScroll 事件,根据 scrollTop 来计算渲染区域向上偏移量。
- 通过重新计算 end 和 start 来重新渲染列表。
function VirtualList(){
const [ dataList,setDataList ] = React.useState([]) /* 保存数据源 */
const [ position , setPosition ] = React.useState([0,0]) /* 截取缓冲区 + 视图区索引 */
const scroll = React.useRef(null)
const box = React.useRef(null)
const context = React.useRef(null)
const scrollInfo = React.useRef({
height:500, /* 容器高度 */
bufferCount:8, /* 缓冲区个数 */
itemHeight:60, /* 每一个item高度 */
renderCount:0, /* 渲染区个数 */
})
React.useEffect(()=>{
const height = box.current.offsetHeight
const { itemHeight , bufferCount } = scrollInfo.current
const renderCount = Math.ceil(height / itemHeight) + bufferCount
scrollInfo.current = { renderCount,height,bufferCount,itemHeight }
const dataList = new Array(10000).fill(1).map((item,index)=> index + 1 )
setDataList(dataList)
setPosition([0,renderCount])
},[])
const handleScroll = () => {
const { scrollTop } = box.current
const { itemHeight , renderCount } = scrollInfo.current
const currentOffset = scrollTop - (scrollTop % itemHeight)
const start = Math.floor(scrollTop / itemHeight)
context.current.style.transform = `translate3d(0, ${currentOffset}px, 0)`
const end = Math.floor(scrollTop / itemHeight + renderCount + 1)
if(end !== position[1] || start !== position[0] ){ /* 如果render内容发生改变,那么截取 */
setPosition([ start , end ])
}
}
const { itemHeight , height } = scrollInfo.current
const [ start ,end ] = position
const renderList = dataList.slice(start,end)
return <div className="list_box" ref={box} onScroll={ handleScroll } >
<div className="scroll_box" style={{ height: height + 'px' }} ref={scroll} >
<div className="context" ref={context}>
{
renderList.map((item,index)=> <div className="list" key={index} > {item + '' } Item </div>)
}
</div>
<div className="scroll_hold" style={{ height: `${dataList.length * itemHeight}px` }} />
</div>
</div>
}
export default VirtualList