React 15架构
架构分为两层:
- Reconciler(调和器):负责找出变化的组件
- Renderer(渲染器):负责将变化的组件渲染到页面上
渲染流程:
Reconciler(调和器)
React 15中又称为 Stack Reconciler。
当this.setState
、this.forceUpdate
、ReactDOM.render
等API触发状态更新后,Reconciler会递归连续不可中断得完成如下工作:
- 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM
- 将虚拟DOM和上次更新时的虚拟DOM进行同步递归对比(React Diff算法)
- 通过对比找出本次更新中变化的虚拟DOM
- 通知Renderer将变化的虚拟DOM渲染到页面上
递归过程中的连续不可中断容易导致用户交互出现卡顿,这里简单做下分析。
回顾下浏览器渲染工作原理:
浏览器刷新频率为60Hz,即每16.6ms(1000ms / 60Hz)浏览器刷新一次。
由于GUI渲染线程与JS线程是互斥的,所以JS脚本执行和浏览器布局、绘制不能同时执行。
即在每16.6ms时间内,需要完成如下工作:JS脚本执行 -----> 样式布局 -----> 样式绘制
当JS脚本执行
消耗时长超出16.6ms时,该次刷新就无法完成样式布局
与样式绘制
。
从中可看出,倘若Reconciler的工作内容非常耗时,即JS脚本执行
阶段耗时超出16.6ms时,会阻塞整个线程。
Stack Reconciler的主要缺陷也十分明显:
- 无法暂停渲染任务
- 无法切分任务
- 无法有效平衡组件更新渲染与动画相关任务间的执行顺序。即不能划分任务优先级,有可能导致重要任务卡顿,动画掉帧等问题。
Renderer(渲染器)
React支持跨平台,不同平台有不同的Renderer(渲染器)。
- ReactDOM渲染器:渲染浏览器DOM
- ReactNative渲染器:渲染App原生组件
- ReactTest渲染器:渲染出纯Js对象用于测试
- ReactArt渲染器:渲染到Canvas, SVG 或 VML (IE8)
在每次更新发生时,Renderer
接到Reconciler
通知,将变化的组件渲染在当前宿主环境。
React 16架构
架构分为三层:
- Scheduler(调度器):调度任务的优先级,高优先级任务优先进入Reconciler
- Reconciler(调和器):负责找出变化的组件
- Renderer(渲染器):负责将变化的组件渲染到页面上
渲染流程:
Scheduler
上一节已知,每16.6ms浏览器刷新一次,即为一帧。浏览器线程执行任务时会以帧的形式划分,在两个执行帧之间,主线程通常会有一小段空闲时间,称为空闲期(Idle Period)。
目前主流浏览器提供了 requestIdleCallback
API,支持在空闲期内调用空闲期回调(Idle Callback),执行一些任务。与之相对的,高优先级任务由requestAnimationFrame
API执行,如动画等。
利用任务优先级高低,分别调度执行函数,可解决React 15的交互卡顿问题。
React 16 基于这个原理实现了功能更完备的requestIdleCallback polyfill
,也就是Scheduler
。
Reconciler
React 16 的Reconciler
,又被称为 Fiber Reconciler。
Fiber Reconciler 通过引入新的数据结构——Fiber对象,配合Scheduler
解决了Stack Reconciler的缺陷。即渲染进程可分段完成,而不必一次性完成,期间优先执行其他高优先级任务。
观察源码可以发现,更新工作从递归变成了可以中断的循环过程。每次循环都会调用shouldYield
判断当前是否有剩余时间。
/** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
} }
那么,Fiber对象如何实现渲染进程可分段完成呢?
- 首先,DOM组件实例均对应一个fiber实例,fiber实例负责管理组件实例的更新,渲染任务及与其他fiber实例的联系。
- 其次,fiber的工作是在内存中进行的,工作的结果交由
Render
处理。
由于是在内存中加工处理fiber实例,本质是对数据的处理,其工作也就可以被暂停,且支持优先级调度。
在对数据的处理过程中,Reconciler
会为需要变化的虚拟DOM打上代表增/删/更新的标记。
// You can change the rest (and add more).
export const Placement = /* */ 0b000000000000010;
export const Update = /* */ 0b000000000000100;
export const PlacementAndUpdate = /* */ 0b000000000000110;
export const Deletion = /* */ 0b000000000001000;
export const ContentReset = /* */ 0b000000000010000;
export const Callback = /* */ 0b000000000100000;
export const DidCapture = /* */ 0b000000001000000;
export const Ref = /* */ 0b000000010000000;
export const Snapshot = /* */ 0b000000100000000;
export const Passive = /* */ 0b000001000000000;
export const PassiveUnmountPendingDev = /* */ 0b010000000000000;
export const Hydrating = /* */ 0b000010000000000;
export const HydratingAndUpdate = /* */ 0b000010000000100;
Render
Renderer
与React 15 的Renderer
类似。不同的是,根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作。
DOM的操作主要分为:
- 插入DOM节点(Placement)
- 更新DOM节点(Update)
- 删除DOM节点(Deletion)
视觉差异
从实际视觉Demo中可以明显看出Stack Reconciler
与 Fiber Reconciler
的差异性。
此处引用 https:// github.com/claudiopro/r eact-fiber-vs-stack-demo
React15中,由于大量的同步计算任务阻塞了浏览器的 UI 渲染,造成页面卡顿。
相比之下,React16的效果非常流畅。但由于Facebook比较慎重,在16的前期版本中默认没有启用该功能,但可以在Dev环境下手动开启。
参考
- https://react.iamkasong.com/
- https://github.com/claudiopro/react-fiber-vs-stack-demo
如果觉得文章能帮助你理解一些前端知识点,请关注微信公众号。