并发和并行
- 并发指的是:多个任务,在同一时间段内同时发生,多个任务会有互相抢占资源的情况。
- 并行指的是:多个任务,在同一时间点上同时发生,多个任务之间不会互相抢占资源。
在这里,我们千万不要理解 Concurrent Mode
是在同时执行多个任务,相信很多人包括我在内都理解混淆,在看源码后,才恍然大悟。
它是同一段时间内,可以执行多个任务:
- 因为高优先级任务可以插队并打断低优先级任务
- 或当时间切片的某一条任务执行超时后,将执行权交给主线程,当主线程执行完成,又继续执行之前的切片任务
为什么需要并发模式
我们知道在同步模式下,从 setState
到 虚拟DOM遍历
,再到 真实DOM
更新,整个过程都是同步执行且无法被中断的,这样可能就会出现一个问题 —— 用户事件触发的更新被阻塞。
因为我们期望一些不重要的更新不会影响一些重要的更新,比如长列表渲染不会阻塞用户 input
输入,从而提升用户体验。
并发模式特点
可中断
对于 React
来说,任务可能很多,如果不区分优先级,那就是先来后到的顺序。如果高优先级任务来了,但是低优先级任务还没有处理完毕,就会造成高优先级任务等待的局面。
由此可见,对于复杂项目来说,任务可中断这件事情很重要。那么问题来了,React
是如何做到的呢,其实基础还是 fiber
,fiber
本身链表结构,就是指针,想指向别的地方加个属性值就行了。
可遗弃
在 Concurrent 模式
下,有些update可能会被遗弃掉。
比如我想在淘宝搜索“老人与海”,那么我在输入框输入“老人与海”的过程中,“老人”会有对应的模糊查询结果,但是不一定是我想要的结果,所以这个时候的模糊查询框的update就是低优先级,“老人”对应UI的update相对input的update,优先级就会低一些。在现在React18中,这个模糊查询相关的UI可以被当做transition。
状态复用
在 Concurrent 模式
下,还支持状态的复用。某些情况下,比如用户走了,又回来,那么上一次的页面状态应当被保存下来,而不是完全从头再来。当然实际情况下不能缓存所有的页面,不然内存不得爆炸,所以还得做成可选的。
目前, React
正在用 Offscreen
组件来实现这个功能。
另外,使用 OffScreen
,除了可以复用原先的状态,我们也可以使用它来当做新UI的缓存准备,就是虽然新UI还没登场,但是可以先在后台准备着,这样一旦轮到它,就可以立马快速地渲染出来。
如何实现并发模式(Concurrent Mode)
Concurrent Mode
这种中断渲染的行为,带来了几个关键问题:
- 怎样做到中断渲染?
- 怎样定义任务的重要程度和执行顺序?
- 何时中断任务,怎样划分时间片?
个人总结主要通过三个方面完成:
- 颗粒化更新节点来解决递归不可中断问题;
- 任务增加优先级来解决任务执行顺序;
- 创建任务调度机制来解决时间分片和任务中断,任务恢复;
对应到React的实现就是:Fiber架构
,lane模型
,scheduler任务调度
Fiber架构
在重构 Fiber
架构之前,React
是没办法解决这些问题的。因为在此之前,React
的渲染更新主要是通过对比更新前后的 虚拟DOM
,找出不同进行更新,而对比的过程因为 虚拟DOM
树结构的限制,只能采用递归更新,我们知道递归一旦开始,中途就无法中断。
那 Fiber
架构为什么能解决这个问题呢?
- 每个
Fiber
节点对应一个React Element
,保存有该组件的所有基本状态信息; - 每个
Fiber
节点保存有该组件的更新信息;
因为 Fiber
节点承载了基本状态和更新信息,这样 React
就可以将 Fiber
节点视为最小的工作单元,可以实现 Fiber
节点这种粒度的更新,因为粒度的细化也就使得异步可中断更新成为了可能。
Fiber
节点的基本状态保存了它的父节点,子节点,兄弟节点信息,这样可以将之前的递归遍历改变为循环遍历,使渲染中断成为可能。
Lane模型
lane模型
主要解决的是任务优先级问题。
我们想中断渲染的本质是想让有更高优先级的任务可以中断低优先级任务来插队执行。
那怎么定义任务优先级呢,lane模型
通过31位的位运算符来定义:
1 | // * lane 值越小,优先级越高 |
可以看到 React
定义的优先级:
同步任务 > 连续触发事件任务 > setTimeout,请求更新任务 > 过渡任务(React18新特性)
事件优先级
1 | export function getEventPriority(domEventName: DOMEventName): * { |
EventPriority | Lane | 数值 | |
---|---|---|---|
DiscreteEventPriority | 离散事件。click、keydown、focusin等,事件的触发不是连续,可以做到快速响应 | SyncLane | 1 |
ContinuousEventPriority | 连续事件。drag、scroll、mouseover等,事件的是连续触发的,快速响应可能会阻塞渲染,优先级较离散事件低 | InputContinuousLane | 4 |
DefaultEventPriority | 默认的事件优先级 | DefaultLane | 16 |
IdleEventPriority | 空闲的优先级 | IdleLane | 536870912 |
调度优先级
SchedulerPriority | EventPriority | 大于>17.0.2 | 小于>17.0.2 |
---|---|---|---|
ImmediatePriority | DiscreteEventPriority | 1 | 99 |
UserblockingPriority | Userblocking | 2 | 98 |
NormalPriority | DefaultEventPriority | 3 | 97 |
LowPriority | DefaultEventPriority | 4 | 96 |
IdlePriority | IdleEventPriority | 5 | 95 |
NoPriority | 0 | 90 |
优先级间的转换
lane优先级 转 event优先级(参考 lanesToEventPriority 函数)
- 转换规则:以区间的形式根据传入的lane返回对应的 event 优先级。比如传入的优先级不大于 Discrete 优先级,就返回 Discrete 优先级,以此类推
event优先级 转 scheduler优先级(参考 ensureRootIsScheduled 函数下的 lanesToEventPriority)
- 转换规则:可以参考上面调度优先级表
event优先级 转 lane优先级(参考 getEventPriority 函数)
- 转换规则:对于非离散、连续的事件,会根据一定规则作转换,具体课参考上面 event 优先级表
任务饥饿
任务饥饿是讲一个低优先级的任务一直被高优先级的任务插队,导致这个任务已经过了执行期限依然没有得到执行,在这种情况下,React
会将该任务置为同步渲染任务,在下次更新时立即执行。
任务插队
具体可查看这篇文章 《react18.2批处理场景原理并结合优先级进行的分析》
scheduler任务调度
具体可查看这篇文章 《react18.2调度器scheduler源码分析》