一、React 架构
React15 架构可以分为两层:
- Reconciler 协调:通过 diff 算法递归比较虚拟dom树,找出变化的组件。
- Renderer 渲染:负责将变化的组件渲染到页面上。
React16 的架构分为三层,它新增了 Scheduler 阶段,并且重构了 Reconciler,即 Fiber Reconciler。
- Scheduler 调度:调度任务的优先级,高优先级任务会优先进入 Reconciler。
- Reconciler 协调:负责找出变化的组件,更新工作变成了可以中断的,Reconciler 内部采用了 Fiber 的架构。
- Renderer 渲染:负责将变化的组件渲染到页面上。
二、对 Fiber 的理解
1. Fiber 是什么
Fiber 可以理解成将一个大任务拆分成的一个个小的执行单元。每执行完一个 Fiber,React 就会检查是否还有空余时间,如果没有时间了就将控制权交还给浏览器,而不需要执行完整个任务才去响应用户。
2. 为什么 React16 要设计 fiber
-
首先
JS引擎和GUI渲染引擎在同一个线程上,两者是互斥关系。如果 js 执行的时间比较长,那么就会阻塞页面的渲染,出现卡顿的现象。 -
v16之前 react 通过对比虚拟DOM树找出需要变动的节点、并更新到页面上,这个协调过程(reconcilation) 是不能被打断的。所以在reconcilation期间 react 会一直占用浏览器资源,导致用户触发的事件得不到及时响应。 -
为了解决以上问题,v16 设计了 Fiber 架构来让协调过程变成可被中断的。它将渲染更新过程拆分成一个个小块的任务,通过合理的调度机制去指定任务执行的时机、让出 CPU 执行权,从而提高浏览器的用户响应速率。
忽略内容:通常流畅动画的帧率是60HZ,1帧约16ms。
3. Fiber 的调度过程
-
优先级:每个更新任务都会设置一个优先级。当任务抵达调度器时,高优先级的更新任务会先进入到 Reconciler 层。
-
可中断:当有更高优先级的更新任务抵达调度器时,Reconciler 层的任务就会被中断,然后调度器将更高级的任务推入 Reconciler 层。
-
Reconciler 层 的详细过程为 三、Fiber Reconciler。
可恢复:当前任务完成渲染后,新一轮的调度开始。之前被中断的任务将会被重新推入 Reconciler 层,继续渲染,即“可恢复”。
三、Fiber Reconciler
协调过程分两个阶段:
1. reconciliation/render 阶段
负责找出所有节点的改变,这个阶段是可中断的 (对应早期版本的 diff 过程)。构建workingInProgress的过程就是diff的过程。
-
以“虚拟dom节点”为维度对任务进行拆分,即一个虚拟dom节点对应一个任务。(如果当前节点不需要更新就直接拷贝,需要的话就打上
tag) -
主线程空闲时会调用
requestIdleCallback执行任务。从根节点开始遍历、通过 diff 算法构建 WIP 树,每处理完一个 fiber 会检查是否还有空闲时间,有的话继续处理下一个任务,没有时间就把控制权交还给主线程,直到下一次requestIdleCallback再继续构建 WIP 树。 -
最后产出
Effect list,记录哪些节点更新、增加或删除了。
2. commit 阶段
负责执行所有的改变,这个阶段是不可中断的(对应早期版本的 patch 过程)。根据Effect list将所有更新都 commit 到DOM树上。
为什么commit不能中断,reconciliation可以中断
-
协调阶段执行的任务不会导致任何用户可见的改变,因为
WIP树是一份用户看不见的草稿,所以在这个阶段让出控制权不会有什么问题。 -
而 commit 阶段会批量更新真实dom,用户看得见,所以不能暂停,否则会出现 UI 更新卡顿的现象。
补充了解:current 树/workInProgress 树+双缓冲
双缓冲技术是在内存中构建并直接替换的技术,例如 Fiber 树的构建与替换,其中 current 树和 workInProgress 树的节点通过alternate 属性相连。
- current 树:当前在屏幕中显示的树。
- workInProgress 树:即将显示在页面中的 Fiber 树。当它构建完成后,React 会将它与 current 树进行替换,从而快速更新 dom。
在首次 render 时,React 会创建一棵虚拟 dom 树,并且会为每个 React 元素创建一个 Fiber 节点,最终得到了 Fiber 树(current Tree,链表结构)。
当 setState 再次更新时,会根据 current 树和最新的虚拟 dom ,生成一个新的 Fiber 树(workInProgress Tree)。
(可忽略)在后续的更新过程中,每次重新渲染都会重新创建 Element,但是 Fiber 不会,它只会使用对应的 Element 中的数据来更新自己的属性。
四、 实现调度的两个浏览器API
1. requestIdleCallback:适用于低优先级
requestIdleCallback will schedule work when there is free time at the end of a frame, or when the user is inactive.
requestIdleCallback是 react fiber 实现的基础 api,它告诉浏览器在 “有空” 的时候就执行我们的回调。(控制权让出)
window.requestIdleCallback (
callback: (dealine: IdleDeadline) => void,
option?: {timeout: number}
)
interface IdleDealine {
didTimeout: boolean // 表示任务执行是否超时
timeRemaining(): DOMHighResTimeStamp // 当前帧还剩多少时间供任务执行
}
// 示例
window.requestIdleCallback(callback, {timeout: 1000 });
具体的执行流程为:
-
开发者使用
requestIdleCallback方法注册对应的任务,告诉浏览器这个任务优先级不高,如果每一帧中存在空闲时间,就可以执行这个任务。浏览器执行完该任务后,如果没有剩余时间了,或者已经没有下一个可执行的任务了,那么 react 应该归还控制权。 -
另外,为了避免浏览器繁忙导致任务一直不执行的情况,开发者可以传入
timeout参数去定义超时时间,如果到了超时时间,浏览器必须立即执行。
那浏览器什么时候有空? 参考《浏览器的每一帧都需要做什么》
requestIdleCallback目前只有 chrome 支持,所以 React 团队实现了功能更完备的polyfill,这就是 Scheduler,它提供了多种调度优先级供任务设置。
polyfill是用于实现浏览器并不支持的原生API的代码。
2. requestAnimationFrame:适用于高优先级任务
requestAnimationFrame是浏览器提供的用于绘制动画的 api,它要求在浏览器在 下次重绘之前(即下一帧) 调用指定的回调函数来更新动画。
两者区别:
requestAnimationFrame的回调属于高优先级任务,在每一帧都会执行;而requestIdleCallback的回调属于低优先级任务,不一定在每帧都执行。
其他
1. Scheduler 中的优先级
自上而下,任务优先级会降低。
- Immediate:立即执行,级别最高。
- UserBlocking:用户阻塞级别的优先级,比如打字、鼠标拖拽。
- Normal:正常
- Low:较低
- Idle:优先级最低,表示任务可以闲置
2. 深度剖析“中断又恢复”的问题
- 中断:保存当前的处理成果,修改
tag标记一下,然后迅速收尾再打开一个requestIdleCallback,下次有机会再做。 - 断点恢复:下次处理到这个
fiber时,看到tag是被打断的任务,那么就接着做未完成的部分 或者 重做。
无论是时间用完了,还是被高优先级任务打断了,对中断机制来说都是一样的。
参考文章:
- Inside Fiber: in-depth overview of the new reconciliation algorithm in React
- 完全理解React Fiber
- Deep In React之浅谈 React Fiber 架构(一)
- Fiber原理介绍
- Fiber的六个问题
新增参考: