13-优化篇-处理海量数据
课程
1
写给想要进阶的你
已学完
学习时长: 4分28秒
2
基础篇-认识 jsx
已学完
学习时长: 40分51秒
3
基础篇-起源 Component
已学完
学习时长: 32分50秒
4
基础篇-玄学 state
已学完
学习时长: 33分37秒
5
基础篇-深入 props
已学完
学习时长: 43分58秒
6
基础篇-理解 lifeCycle
学习时长: 72分46秒
7
基础篇-多功能 Ref
已学完
学习时长: 71分17秒
8
基础篇-提供者 context
已学完
学习时长: 53分16秒
9
基础篇-模块化 css
学习时长: 35分20秒
10
基础篇-高阶组件
学习时长: 43分29秒
11
优化篇-渲染控制
学习时长: 50分4秒
12
优化篇-渲染调优
学习时长: 35分8秒
13
优化篇-处理海量数据
学习时长: 28分1秒
14
优化篇-细节处理(持续)
学习时长: 32分19秒
15
原理篇-事件原理
学习时长: 30分48秒
16
原理篇-调度与时间片
学习时长: 23分47秒
17
原理篇- 调和与 fiber
学习时长: 35分54秒
18
原理篇-Hooks 原理
已学完
学习时长: 34分34秒
19
生态篇-React-router
学习时长: 50分9秒
20
生态篇-React-redux
学习时长: 50分34秒
21
生态篇-React-mobx
学习时长: 65分3秒
22
实践篇-实现mini-Router
学习时长: 37分19秒
23
实践篇-表单验证上
学习时长: 41分49秒
24
实践篇-表单验证下
学习时长: 46分31秒
25
实践篇-自定义弹窗
学习时长: 35分4秒
26
实践篇-自定义 Hooks 设计(持续)
学习时长: 32分13秒
27
实践篇-自定义 Hooks 实践(持续)
学习时长: 58分47秒
28
总结篇-如何有效阅读源码
学习时长: 7分18秒
29
原理篇-Context原理
学习时长: 28分27秒
30
原理篇-beginWork和render全流程
学习时长: 38分16秒
31
V18特性篇-useMutableSource(已被取缔)
学习时长: 28分37秒
32
V18特性篇-transition
学习时长: 38分15秒
33
原理篇-更新流程:进入调度任务
学习时长: 30分20秒
34
v18特性篇-concurrent 下的 state更新流程
学习时长: 29分27秒
35
v18特性篇-订阅外部数据源
学习时长: 16分53秒
36
原理篇-v18commit全流程
学习时长: 45分40秒
37
v18新特性-Offscreen
学习时长: 1秒
38
实践篇-设计并实现 keepalive 功能
学习时长: 71分13秒
juejin_logo copyCreated with Sketch.

前言

本章节将要介绍一下 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元素容器的宽高,渲染列表。

效果:

2.gif

可以直观看到这种方式渲染的速度特别慢,而且是一次性突然出现,体验不好,所以接下来要用时间分片做性能优化。

// 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 形式,会出现丢帧的情况,在真实场景,体验感更好):

3.gif

实践二 虚拟列表

虚拟列表是一种长列表的解决方案,现在滑动加载是 M 端和 PC 端一种常见的数据请求加载场景,这种数据交互有一个问题就是,如果没经过处理,加载完成后数据展示的元素,都显示在页面上,如果伴随着数据量越来越大,会使页面中的 DOM 元素越来越多,即便是像 React 可以良好运用 diff 来复用老节点,但也不能保证大量的 diff 带来的性能开销。所以虚拟列表的出现,就是解决大量 DOM 存在,带来的性能问题。

何为虚拟列表,就是在长列表滚动过程中,只有视图区域显示的是真实 DOM ,滚动过程中,不断截取视图的有效区域,让人视觉上感觉列表是在滚动。达到无限滚动的效果。

虚拟列表划分可以分为三个区域:视图区 + 缓冲区 + 虚拟区。

1.jpg

  • 视图区:视图区就是能够直观看到的列表区,此时的元素都是真实的 DOM 元素。
  • 缓冲区:缓冲区是为了防止用户上滑或者下滑过程中,出现白屏等效果。(缓冲区和视图区为渲染真实的 DOM )
  • 虚拟区:对于用户看不见的区域(除了缓冲区),剩下的区域,不需要渲染真实的 DOM 元素。虚拟列表就是通过这个方式来减少页面上 DOM 元素的数量。

具体实现思路。

  • 通过 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)  /* 获取scroll元素 */
   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 } = scroll.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) /* 渲染区间 */
   console.log('渲染区间',position)
   return <div className="list_box" ref={box} >
     <div className="scroll_box" style={{ height: height + 'px'  }}  onScroll={ handleScroll } ref={scroll}  >
        <div className="scroll_hold" style={{ height: `${dataList.length * itemHeight}px` }}  />
        <div className="context" ref={context}> 
            {
               renderList.map((item,index)=> <div className="list" key={index} >  {item + '' } Item </div>)
            }  
        </div>
     </div>
   </div>
}

完美达到效果:

4.gif

总结

对于海量的数据处理,在实际项目中,可能会更加复杂,本章节给了两个海量数据场景的处理方案,时间分片( Time slicing )和虚拟列表( Virtual list ),如果真实项目中有这个场景,希望能给大家一个处理思路。纸上得来终觉浅,绝知此事须躬行。

留言
Ctrl + Enter
全部评论(62)
lleung的头像
删除
字节跳动
/6666
点赞
回复
stan900814的头像
删除
菜🐔前端
为什么我照着github上的最新的时间分片的代码来的。但是跑不起来呢。[发呆]
点赞
回复
themeth的头像
删除
补充下虚拟列表源码中遗漏的可能样式
.scroll_box {
overflow: auto;
}
.scroll_hold {
float: left;
}
.list_box 中的overflow修改为hidden,避免外面产生一层滚动。
展开
1
回复
Aweiweier192_的头像
删除
虚拟列表,丢失的样式:
.scroll_box {
overflow: scroll;
position: relative;
}

.scroll_hold {
position: absolute;
left: 0;
top: 0;
right: 0;
}
展开
1
回复
不气盛叫年轻人么的头像
删除
前端全干工程师
通过 renderList 把已经渲染的 element 缓存起来,渲染控制章节讲过,这种方式可以直接跳过下一次的渲染。麻烦问下,这个具体是怎么实现的?
点赞
1
删除
缓存起来就是个数组,render中Fiber调和时,旧有的由于是被state缓存的, props对比Fiber上的props是不变的,所以跳过更新。新push到数组里的继续调和,所以只渲染新push到数组里的组件
点赞
回复
尤可脱的头像
删除
Web前端开发工程师
这个长列表渲染解决方案,有个缺点:
1. 每次滚动都会 重新setPosition([start, end]); dataList.slice(start, end);
2. 只要一滚动就会重新渲染可视区内的元素;假如可视区域有300个div元素, div中又有一些元素以及数据展示;那么每次滚动都会重新渲染这些内容;性能势必打折;
3. new IntersectionObserver(); 可以获取到 列表出现在可视区,以及消失的状态; 这样滚动时,只要渲染新出现可视区的元素,以及把滚动出可视区元素置为空div 就行; 避免每次都setData(list);
3
回复
ronnycyy的头像
删除
前端开发工程师
您好有个地方我有点疑惑: 虚拟长列表里 真实列表为什么用 transform: translate3d(x,y,z),换成 translate(x,y)不可以吗?
1
2
删除
可以的。[可怜]
点赞
回复
删除
使用 transform: translate3d(x,y,z),可以将该渲染层提升为合成层,开启GPU加速,算是一个优化。
点赞
回复
ronnycyy的头像
删除
前端开发工程师
github上虚拟列表组件缺失了一些样式,已提交PR,有空看下
点赞
1
删除
好的 感谢大佬啦~
点赞
回复
xulif的头像
删除
const currentOffset = scrollTop - (scrollTop % itemHeight)
这行代码的作用是什么?直接用scrollTop用作transform的偏移量就没有那么的丝滑呀?
1
1
删除
因为直接使用的话,就每触发一次scroll事件就会改变下偏移量,造成滑动与偏移同时产生,所以就不会这么丝滑了。取余操作的话,表示每经过一个元素块变动一次,不会频繁的触发偏移操作
点赞
回复
花总的头像
删除
高级开发工程师 @ 某金融独角兽公司
加油
点赞
回复
前端小菜鸟Bob的头像
删除
前端
GitHub上下载的demo跑起来,效果不对啊
点赞
回复
YYYZZZHHH的头像
删除
为什么我在实现时间分片的时候,后面几次的渲染会特别的慢
点赞
1
删除
不是已经分片渲染了嘛,并且还用key标记了之前渲染的片段,那么应该每一次渲染的耗时是一样的吧
点赞
回复
Alisa不想说话的头像
删除
虚拟列表中的div className="scroll_hold" style={{ height: `${dataList.length * itemHeight}px` }} 这个是不是应该去掉[哭笑]
点赞
1
删除
去掉,滚动条显示就有问题,这里需要 div 占位,撑起滚动条。
点赞
回复
保密的头像
删除
前端开发
将class为scroll_hold 元素移到class为context 的下面,将onScroll事件放到class为list_box的元素上
点赞
回复
保密的头像
删除
前端开发
return. 地方的代码也需要改一下,评论发不了html代码?
点赞
回复
保密的头像
删除
前端开发
作者提供的代码使用不了,对应部分需修改成下面代码:
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 ])
}
}
展开
点赞
回复
gilxyj的头像
删除
Java开发
renderList.push(this.renderNewList(index)) /* 通过缓存element把所有渲染完成的list缓存下来,下一次更新,直接跳过渲染 */
这个"把所有渲染完成的list缓存下来"是指 key={index} 的react.Fragment 吗?大佬
1
2
删除
应该指的就是fragment生成的代码片段,可以视为一个组件,不对比内部元素
点赞
回复
删除
fiber 渲染的时候会进行比较 你在去渲染的时候你的key没有发生变化 react 认为你没有变化 react会跳过 不会渲染,去渲染新的dom
点赞
回复
yx释然的头像
删除
虚拟列表代码运行看不到效果
2
回复
王冰冰yo的头像
删除
前端顶级摸鱼师
计算偏移量真的很妙,不是这样算的话,就没有那么丝滑[哭笑]
点赞
2
删除
没有太理解为啥要计算诶[泣不成声]
点赞
回复
删除
同问,为啥要这么算?
点赞
回复
js-ding的头像
删除
web前端
Math.floor(Math.random()*255)这个取不到255,Math.random()取值不包含1
1
回复

查看全部 62 条回复