竹笋

首页 » 问答 » 环境 » React161718优化点大汇总
TUhjnbcbe - 2022/12/24 19:13:00

React16

主要有九点新的调度方式~Fiber;生命周期;context;严格模式;portal;refs;fragment;hook;suspense;

一.Fiber架构

背景知识

一)reactFiber在react中的角色

react核心理念分为三个部分:

1.ReactCore:处理最核心的APIs,与终端平台解耦

2.ReactRender:渲染器定义了一个ReactTree,来处理如何接轨不同的平台比如,react-dom渲染为浏览器DOM;ReactNative渲染为原生视图;

3.Reconciler:负责diff算法,进行patch行为。可以被各种render公用,提供基础的计算能力。目前主要有两类reconciler:(1)stackreconciler,15以及更早期的版本使用(2)Fiberreconciler,新一代的架构;Fiber就是为了解决之前react的问题,进行组件渲染时,从setState开始到渲染完成整个过程是同步的。如果需要渲染的组件比较庞大,js执行会阻塞主线程,会导致页面响应度变差,使得react在动画、手势等应用中效果比较差。

核心思想~~1.让渲染有优先级;2.可中断

二)单核处理器(模拟JS)并发和并行的区别

(1)并发,Concurrent

操作系统会按照一定的调度策略,将CPU的执行权分配给多个进程,让它们交替执行,形成一种“同时在运行”假象,因为CPU速度太快,人类感觉不到。实际上在单核的物理环境下同时只能有一个程序在运行。

(2)并行

真正实现同时处理多个任务,这就是并行,严格地讲这是Master-Slave架构,分身虽然物理存在,但应该没有独立的意志;

三)调度策略

1.单核处理器常见的调度策略

(1)先到先得(First-Come-First-Served,FCFS);对短进程、IO密集型不利;(2)轮询,要设置好时间片的长度;最好符合大部分进程完成一次典型交互所需的时间;对IO不利;(3)最短进程优先(ShortestProcessNext,SPN)可能长进程得不到响应;长进程阻塞后面;(4)最短剩余时间(ShortestRemainingTime,SRT)增加抢占机制,剩余时间短,新的就会抢占旧的进程;(5)最高响应比优先(HRRN)响应比=(等待执行时间+进程执行时间)/进程执行时间(6)反馈法每个进程一开始都有相同的优先级,每次被抢占(需要配合其他抢占策略使用,如轮转),优先级就会降低一级。因此通常它会根据优先级划分多个队列。

2.浏览器没有抢占机制,Reactfiber用的主动出让机制

(1)合作式调度cooperativescheduling(2)用户代码向浏览器申请时间片,由浏览器给我们分配执行时间片(类似requestIdleCallback实现,),我们要按照约定在这个时间内执行完毕,并将控制权还给浏览器。

四)Fiber协程

1.和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的出让机制。

2.ES6的Generator普通函数无法中断,generator可以;

3.为啥不用generator实现(1)Generator不能在栈中间让出。比如你想在嵌套的函数调用中间让出,首先你需要将这些函数都包装成Generator,另外这种栈中间的让出处理起来也比较麻烦,难以理解。除了语法开销,现有的生成器实现开销比较大,所以不如不用。(2)Generator是有状态的,很难在中间恢复这些状态。

一.时间切片TimeSlicing

1.作用

(1)在执行任务过程中,不阻塞用户与页面交互,立即响应交互事件和需要执行的代码;(2)实现高优先级插队;

2.源码细节

(1)letyieldInterval=5;五毫秒,目前单位时间切片的长度;(2)源码注释中有说明,一个帧中会有多个时间切片(一帧~=16.67ms,包含3个时间切片还多)切片时间不会与帧对齐,如果要与帧对齐,则使用requestAnimationFrame~~RAF,因为浏览器自动在每帧开头调用,就跟帧对齐了;

二.requestIdleCallback~RIC

1.概念

(1)超时检查机制目前浏览器无法判断当前是否有更高优先级的任务等待被执行,只能换一种思路,通过浏览器提供的requestIdleCallbackAPI超时检查机制来让出控制权;确定一个合理的运行时长~上一点的时间切片timeslicing,每隔5毫秒,检测是否超时(比如每执行一个小任务),如果超时就停止执行,将控制权交换给浏览器。(2)理想的一帧时间是16ms(ms/60),如果浏览器处理完布局和绘制等任务之后,还有盈余时间,浏览器就会调用requestIdleCallback的回调(3)在浏览器繁忙的时候,可能不会有盈余时间,这时候requestIdleCallback回调可能就不会被执行。为了避免其他任务饿死,可以通过requestIdleCallback的第二个参数指定一个超时时间timeout,超过timeout时长后,该回调函数会被立即执行;(4)类似于rAF返回一个句柄,可以把它传入cancelIdleCallback取消掉任务

2.使用

(1)接口

(2)伪代码:

3.使用建议

(1)只在低优先级的任务中使用它,因为你无法控制它的执行时机。比如给后台发送一些不怎么重要的监控数据,或者进行某种页面检查。(2)不要在其中修改DOM元素,因为它在一个任务周期的layout结束之后才执行,如果你修改了DOM,则会再次引发重排,这会对性能产生一定的影响。推荐的做法是创建一个documentFragment保存对dom的修改,并注册requestAnimationFrame来应用这些修改。(3)不在其中执行难以预测执行时间的任务,比如以Promise的形式执行某个接口请求。(4)只在必需时使用timeout选项,浏览器会花费额外的开销在检查是否超时上,产生一些性能损失。

4.兼容性

(1)目前requestIdleCallback目前只有Chrome支持。(2)而且RIC调用频率是20次/秒,远低于页面流畅度的要求,每次是ms/20=50ms的计算时间,如果完全用这50ms来计算,同样会带来交互上的卡顿;(3)所以目前React自己实现了一个;

三.ReactScheduler的源码实现

1.MessageChannel

(1)利用MessageChannel(宏任务)模拟将回调延迟到一帧的最后再执行:(2)源码中使用setTimeout作为降级方案;(3)MessageChannel构造函数返回一个带有两个MessagePort属性的MessageChannel新对象。允许我们创建一个新的消息通道,并通过它的两个MessagePort属性发送数据。

2.ReactScheduler使用MessageChannel的原因

(1)生成宏任务

将主线程还给浏览器,以便浏览器更新页面。浏览器更新页面后继续执行未完成的任务。

(2)为什么不使用微任务呢?

微任务将在页面更新前全部执行完,所以达不到「将主线程还给浏览器」的目的。

(3)为什么不使用setTimeout(fn,0)

递归的setTimeout()调用会使调用间隔变为4ms,导致浪费了4ms。

(4)为什么不使用rAF()

如果上次任务调度不是rAF()触发的,将导致在当前帧更新前进行两次任务调度。页面更新的时间不确定,如果浏览器间隔了10ms才更新页面,那么这10ms就浪费了。

3.unstable_scheduleCallback

unstable_scheduleCallback(priorityLevel/delay,callback,{timeout:number})

(1)维护两个小顶堆taskQueue和timerQueue,前者保存等待被调度的任务,后者保存调度中的任务

(2)unstable_scheduleCallback提供两个参数delay表示任务的超时时长;timeout表示任务的过期时长;如果没有指定,根据优先程度任务会被分配默认的timeout时长。

(3)如果没有提供delay,则任务被直接放到taskQueue中等待处理;

(4)如果提供了delay,则任务被放置在timerQueue中,

(5)此时如果taskQueue为空,且当前任务在timerQueue的堆顶(当前任务的超时时间最近),则使用requestHostTimeout启动定时器(setTimeout),在到达当前任务的超时时间时执行handleTimeout,此函数调用advanceTimers将timerQueue中的任务转移到taskQueue中,

(6)此时如果taskQueue没有开启执行,则调用requestHostCallback启动它,否则继续递归地执行handleTimeout处理下一个timerQueue中的任务。

(7)时间分片每5毫秒检查一下,运行一次异步的MessageChannel的port.postMessage(…)方法,检查是否存在事件响应、更高优先级任务或其他代码需要执行;如果有则执行,如果没有则重新创建工作循环,执行剩下的工作中Fiber。

(8)5ms在频繁交互的页面不错;如果页面基本是静态的,可以将时间分片拉长;react中利用了一个支持度不算太高的BOMAPI~navigator.scheduling.isInputPending,表示用户的输入是否被挂起,也就是用户输入没有及时得到反馈。如果页面没有发生交互,且不需要重绘(needsPaint===false,这是程序内的一个全局变量),则React会把时间分片提升到ms(maxYieldInterval)虽然这个时间远超反应延迟,但是taskQueue中每一项任务执行完成后都会去检测有没有用户交互和重绘,如果有则立即把资源回交给浏览器,所以不用担心会因此发生卡顿。

四.过期时间ExpirationTime

1.会根据当前优先级和当前时间标记生成对应过期时间标记2.两种类型(1)时间标记:一个极大值,如(2)过期时间:从网页加载开始计时的实际过期时间,单位为毫秒(3)过期时间标记简单理解为和过期时间成反比;过期时间=当前时间+优先级对应过期时长过期时间标记=极大数值-过期时间/10

五.任务优先级

1.优先级从高到底,0-5

(1)NoPriority,初始化、重置root、占位用;(2)ImmediatePriority(-1),这个优先级的任务会立即同步执行,且不能中断,用来执行过期任务;(3)UserBlockingPriority(ms),会阻塞渲染的优先级,用户交互的结果,需要及时得到反馈(4)NormalPriority(5s),默认和普通的优先级,应对哪些不需要立即感受到的任务,例如网络请求(5)LowPriority(10s),低优先级,这些任务可以放后,但是最终应该得到执行.例如分析通知(6)IdlePriority(没有超时时间),空闲优先级,用户不在意、一些没必要的任务(e.g.比如隐藏的内容),可能会被饿死;

2.react17有个新的更细级别的优先级,lanes车道,

指定一个连续的优先级区间,如果update的优先级在这个优先级区间内,则处理区间内包含的优先级生成对应页面快照。lanes模型使用31位的二进制代表31种可能性。可以分别给IO任务,cpu任务不同的lane,最后可以并发的执行这几种类型的优先级。(1)注意:时间碎片未用完,高优先级的也抢不了,因为没有释放控制权;

六.流程图

七.React为Fiber做的改造

1.数据结构的调整

(1)StackReconciler到FiberReconciler

React16之前,Reconcilation是同步的、递归执行的;现在使用扁平化的链表(单链表)的数据存储结构,使用循环迭代来代替之前的递归;链表比顺序结构数据更占用空间,空间换时间,更方便按照优先级操作,根据当前节点找到下一个节点,方便挂起和恢复;

例子:这样的父子组件的生命周期执行顺序1)react16之前的协调算法,是递归算法先子元素B再父元素A,b.willMountb.didMounta.willMounta.didMount2)react16之后的Fiber架构,是扁平化的链表的数据存储结构,FIFO先进先出,先是父A再是子B,a.willMounta.didMountb.willMountb.didMount

(2)每个VirtualDOM节点内部现在使用Fibernode表示

模拟函数调用栈,保存了节点处理的上下文信息,方便中断和恢复;

最核心的三个指针

child:父节点指向第一个子元素的指针。

sibling:从第一个子元素往后,指向下一个兄弟元素。

return:所有子元素都有的指向父元素的指针。

(3)Fiber也可以理解为工作单元

performUnitOfWork负责对Fiber进行操作,并按照深度遍历的顺序返回下一个Fiber。两层循环,一个从上往下,一个从下往上;因为使用了链表结构,每个节点存了各种状态和数据,即使处理流程被中断了,我们随时可以从上次未处理完的Fiber继续遍历下去。

2.两个阶段的拆分

之前是一边Diff一边提交的,现在分为两个阶段

reconciliation协调阶段和

1
查看完整版本: React161718优化点大汇总