最近学习了react源码,发现社区里面的大部分概念跟react 18的源码实现有些差异,比如effect list、requestIdleCallback等很多概念在新版本(React 18)中已经逐渐移除,理念也发生了变更,所以特此做个归纳总结,方便大家学习,有问题可以随时留言或者私信!(感兴趣的同学也欢迎一起学习)
核心流程如下图所示
React是当前最流行的前端框架,react当中有几个核心的概念:渲染器、调和器和调度器
渲染器
React 最初只是服务于 DOM,但是这之后被改编成也能同时支持原生平台的 React Native。因此,在 React 内部机制中引入了“渲染器”这个概念。
渲染器用于管理一棵 React 树,使其根据底层平台进行不同的调用。
渲染器同样位于 packages/
目录下:
- React DOM Renderer 将 React 组件渲染成 DOM。它实现了全局
ReactDOM
API,这在npm上作为react-dom
包。这也可以作为单独浏览器版本使用,称为react-dom.js
,导出一个ReactDOM
的全局对象. - React Native Renderer 将 React 组件渲染为 Native 视图。此渲染器在 React Native 内部使用。
- React Test Renderer 将 React 组件渲染为 JSON 树。这用于 Jest 的快照测试特性。在 npm 上作为 react-test-renderer 包发布。
另外一个官方支持的渲染器的是 react-art
。它曾经是一个独立的 GitHub 仓库,但是现在我们将此加入了主源代码树。
调和器
即便 React DOM 和 React Native 渲染器的区别很大,但也需要共享一些逻辑。特别是协调算法需要尽可能相似,这样可以让声明式渲染,自定义组件,state,生命周期方法和 refs 等特性,保持跨平台工作一致。
为了解决这个问题,不同的渲染器彼此共享一些代码。我们称 React 的这一部分为 “reconciler”。当处理类似于 setState()
这样的更新时,reconciler 会调用树中组件上的 render()
,然后决定是否进行挂载,更新或是卸载操作。
Reconciler 没有单独的包,因为他们暂时没有公共 API。相反,它们被如 React DOM 和 React Native 的渲染器排除在外。
Stack reconciler
“stack” reconciler 是 React 15 及更早的解决方案。
stack reconciler采用递归的方式创建虚拟DOM并提交Dom Mutation,整个过程同步并且无法中断工作或将其拆分为块。如果组件树的层级很深,递归会占用线程很多时间,递归更新时间超过了16ms,用户交互就会卡顿。
Fiber reconciler
“fiber” reconciler 是一个新尝试,致力于解决 stack reconciler 中固有的问题,同时解决一些历史遗留问题。
Fiber 从 React 16 开始变成了默认的 reconciler。
它的主要目标是:
- 能够把可中断的任务切片处理。
- 能够调整优先级,重置并复用任务。
- 能够在父元素与子元素之间交错处理,以支持 React 中的布局。
- 能够在
render()
中返回多个元素。 - 更好地支持错误边界。
非并发模式进行以下操作:
顶部是个slider,拖放后会怼整个chart区域缩放
火焰图调用信息如下
并发模式进行同样的操作:
火焰图调用信息如下
通过对比,可以很明显的感受到并发模式下的流畅性
调度器
调度器主要包含两块:时间调度、优先级调度
浏览器每一帧需要执行的任务
时间调度
下面是react源码中的几种调度方式,有先后关系
方式一:isInputPending
参考文档:https://wicg.github.io/is-inp...
在运行需要显示某些内容的脚本时,开发人员今天需要做出判断。
如果脚本可能计算很长时间才能运行并且用户在发生这种情况时进行了某种输入,那么浏览器将需要等到脚本完成后才能分派输入事件。这会造成在响应输入事件之前有很长的延迟,用户体验并不是很好,因此开发人员通常会将长脚本任务分解成更小的块,以允许用户代理在块之间调度事件。每次脚本执行时,它都需要以某种方式发布一条消息,调用 requestAnimation 帧和 requestIdleCallback 的组合,或者采用其它方式来生成可以被调度的事情,之后它可以在空闲时再次被调用。即使在最好的情况下,脚本每次产生时也可能需要很多毫秒才能再次运行。所以不幸的是,这也不是一个很好的用户体验,因为部分初始的脚本被延迟了很久才执行,尽管他需要这么久的时间。
为了避免这种取舍,Facebook
在 Chromium
中提出并实现了 isInputPending() API
,它可以提高网页的响应能力,但是不会对性能造成太大影响。
isInputPending api 的目标是它现在将允许开发人员消除这种权衡。不再完全屈服于用户代理,并且在屈服后必须承担一个或多个事件循环的成本,长时间运行的脚本现在可以运行到完成,同时仍然保持响应。
目前 isInputPending API 仅在 Chromium 的 87 版本开始提供,其他浏览器并未实现。
方式二:setImmediate
方式一主要使用在Chromium引擎的浏览器中,方式二从设计上,优先考虑了IE的兼容性
该方法用来把一些需要长时间运行的操作放在一个回调函数里,在浏览器完成后面的其他语句后,就立刻执行这个回调函数。
仅 IE支持
方式三:MessageChannel
Channel Messaging API的MessageChannel
接口允许我们创建一个新的消息通道,并通过它的两个MessagePort
属性发送数据。
在以下示例中,您可以看到使用MessageChannel构造函数实例化了一个channel对象。当iframe加载完毕,我们使用MessagePort.postMessage方法把一条消息和MessageChannel.port2传递给iframe。handleMessage处理程序将会从iframe中(使用MessagePort.onmessage监听事件)接收到信息,将数据其放入innerHTML中。
var channel = new MessageChannel();
var para = document.querySelector('p');
var ifr = document.querySelector('iframe');
var otherWindow = ifr.contentWindow;
ifr.addEventListener("load", iframeLoaded, false);
function iframeLoaded() {
otherWindow.postMessage('Hello from the main page!', '*', [channel.port2]);
}
channel.port1.onmessage = handleMessage;
function handleMessage(e) {
para.innerHTML = e.data;
}
方式四:setTimeout
给大家都懂,略过
调度器的切片时间
切片间隔时间是5ms,最大间隔时间是300ms,源码如下
// Scheduler periodically yields in case there is other work on the main
// thread, like user events. By default, it yields multiple times per frame.
// It does not attempt to align with frame boundaries, since most tasks don't
// need to be frame aligned; for those that do, use requestAnimationFrame.
let yieldInterval = 5;
let deadline = 0;
// TODO: Make this configurable
// TODO: Adjust this based on priority?
const maxYieldInterval = 300;
let needsPaint = false;
切片和React相互关系
任务拆分
将调和阶段(Reconciler)递归遍历 VDOM 这个大任务分成若干小任务,每个任务只负责一个节点的处理。
- workLoopSync or workLoopConcurrent
- performUnitOfWork
任务挂起、恢复、终止
在新 workInProgress tree 的创建过程中,会同 currentFiber 的对应节点进行 Diff 比较,生成对应的falgs,同时也会复用和 currentFiber 对应的节点对象,减少新创建对象带来的开销。也就是说无论是创建还是更新、挂起、恢复以及终止操作都是发生在 workInProgress tree 创建过程中的。workInProgress tree 构建过程其实就是循环的执行任务和创建下一个任务。
挂起
当第一个小任务完成后,先判断这一帧是否还有空闲时间,没有就挂起下一个任务的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务。
恢复
在浏览器渲染完一帧后,判断当前帧是否有剩余时间,如果有就恢复执行之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段。
终止
其实并不是每次更新都会走到提交阶段。当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作
任务优先级
下列是源码中提供的任务优先级,除了无优先任务外,其它任务数值越小优先级越高
// 无优先级任务
export const NoPriority = 0;
// 立即执行任务
export const ImmediatePriority = 1;
// 用户阻塞任务
export const UserBlockingPriority = 2;
// 正常任务
export const NormalPriority = 3;
// 低优先级任务
export const LowPriority = 4;
// 空闲执行任务
export const IdlePriority = 5;