跳到主要内容

认识

2024年03月04日
柏拉文
越努力,越幸运

一、认识


React.js 源码旨在了解、掌握 React 底层设计思想、实现。

二、编译


React.js 17 之前, 应用程序通过 @babel/preset-react JSX 语法转换为 React.createElementjs 代码,因此需要显式将 React 引入,才能正常调用 createElementReact.js 17 版本之后,官方与 babel 进行了合作,直接通过将 react/jsx-runtimejsx 语法进行了新的转换而不依赖 React.createElement, 称为 Runtime Automatic(自动运行时)。在自动运行时模式下,JSX会被转换成新的入口函数,import {jsx as _jsx} from 'react/jsx-runtime';import {jsxs as _jsxs} from 'react/jsx-runtime';

React 中的 createElement 以及 JSX 都会返回一个 ReactElement 对象, 用于后续创建 Fiber 对象。

由上所述, React 是纯运行时前端框架, 在运行前, 已经将 JSX 全部转换为 ReactElement 虚拟 DOM, 在运行中没有机会进行编译优化。

三、初始化


ReactDOM.render()React.createRoot(container) 进入初始化流程

3.1 render()

render() 主要工作是创建整个应用的 FiberRoot, 创建一个 UpdateQueue 放入更新队列, 调用 scheduleUpdateOnFiber 进入调度更新流程。初始化流程如下:

3.2 createRoot()

createRoot() 用于客户端创建 React 渲染的根节点, 有 renderunmount 两个方法。主要工作是创建整个应用的 FiberRoot, 创建一个 UpdateQueue 放入更新队列, 调用 scheduleUpdateOnFiber 进入调度更新流程, 开启并发模式, 开启自动批处理。初始化流程如下:

  1. 创建 root 应用根节点对象 HostRootFiberFiberRootNode, 然后关联两个对象

    • FiberRootNode: 是 React 应用的根节点, 它的作用是负责应用加载相关的内容,比如应用加载模式mode,存储本次应用更新的回调任务以及优先级,存储创建完成的FiberTree等。

    • HostRootFiber: 是虚拟DOM树的根节点,类型是FiberNode, 针对普通 DOM 元素或者组件创建的 Fiber 对象,是虚拟DOM的真实体现

  2. 初始化 HostRootFiber.updateQueue, 创建一个 updateQueue 更新队列

  3. 给根元素绑定了所有事件,任何子孙元素触发的该类型事件都会委托给根元素的事件回调处理

  4. 以同步的优先级, 创建一个 update 对象,包含了更新的信息,将其添加到组件的更新队列中, 开启调度更新流程

四、Scheduler


4.1 更新队列

  1. 初始化 UpdateQueue 队列: 初始化一个 UpdateQueue,并将 updateQueue 赋值给 fiber.updateQueueupdateQueue 队列是 fiber 更新时要执行的内容。

  2. 创建 update: update 保存更新状态的相关内容, 保存有对应的优先级Lane

  3. 进入 UpdateQueue 队列: 向链表 fiber.updateQueue.shared.pending 中添加 Update 节点, 形成一个单向环形链表。之所以设计为单向环形链表, 原因如下:

    1. 做成环形链表可以只需要利用一个指针,便能找到头节点与尾节点

    2. 更加方便地找到最后一个 Update 对象,同时插入新的 Update 对象也非常方便

    3. 如果使用普通的线性链表,就需要同时记录第一个和最后一个节点的位置,维护成本相对较高

4.2 调度更新

  1. 更新触发更新 Fiber 的优先级 childLanes, 递归向上把父级的 childLanes 都更新,更新成当前的任务优先级, 直到 rootFiber, 并返回 rootFiber: 如果 A 组件发生了更新,那么先向上递归更新父级链的 childLanes,接下来从 Root Fiber 向下调和的时候,发现 childLanes 等于当前更新优先级,那么说明它的 child 链上有新的更新任务,则会继续向下调和,反之退出调和流程。因此, 通过 RootFiber 是如何找到需要更新的组件的: Root Fiber 是通过对比每一级的 childLanes 与当前更新优先级, 逐渐向下对比调和找到需要更新的组件的。 问题: 为什么要从触发更新的 Fiber 递归向上形成一个 childLanes 链, 而不可以从从触发的 Fiber 直接向下调和呢? React 并不知道一次 state 变化, 影响的范围, 比如一次更新发生, 父子 Fiber 上都有更新, 如果直接向下调和, 那么是不合理的, 所以采用了从 rootFiber 统一向下的方式。

  2. 标记本次更新任务优先级: 合并 rootFiber.childLanes 与当前更新任务 lane

  3. 基于 rootFiber 开启调度更新

  4. 对比本次更新任务优先级和上一次等待更新任务优先级: 如果相等, 复用本次更新任务调度, 终止调度流程。如果不相等, 继续向下调度

  5. 根据本次更新任务的优先级,决定以同步还是并发的方式调度本次更新: 同步方式将 performSyncWorkOnRoot 同步任务加入到 syncQueue 同步任务队列, 通过 Promise.then 以微任务的方式, 遍历执行 syncQueue 同步任务队列。异步方式通过 Scheduler 中的 scheduleCallback 以本次优先级对应的 Scheduler 调度优先级, 调度执行 performConcurrentWorkOnRoot 异步更新任务。

  6. 对于 scheduleCallback 中的异步任务: 在函数中, 不同的优先级意味着不同时长的任务过期时间。同时会比较 startTimecurrentTime , 如果 startTime > currentTime, 表示当前任务未就绪, 存入 timerQueue。并根据开始时间重新排列 timerQueue 中任务的顺序。当 timerQueue 中有任务就绪,即startTime <= currentTime,我们将其取出并加入taskQueuetimerQueuetaskQueue 两个队列为了能在 O(1) 的时间复杂度里找到两个队列中时间最早的那个任务, 采用的是最小堆的数据结构, 每次存入任务的过程就是最小堆自动调整的过程

4.3 工作单元

// 同步
function performSyncWorkOnRoot(){
renderRoot();

……

commitRoot();
}

// 并发
function performConcurrentWorkOnRoot(){
renderRoot();

……

commitRoot();
}

function renderRoot(root, lane, shouldTimesSlice) {
do {
try {
shouldTimesSlice ? workLoopConcurrent() : workLoopSync();
break;
} catch (e) {
}
} while (true);

return RootCompleted;
}

function workLoopSync() {
while (workInProgress != null) {
performUnitOfWork(workInProgress);
}
}

function workLoopConcurrent() {
while (workInProgress != null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}

function performUnitOfWork(fiber) {
const next = beginWork(fiber, workInProgressRootRenderLane);
fiber.memoizedProps = fiber.pendingProps;

if (next === null) {
completeUnitOfWork(fiber);
} else {
workInProgress = next;
}
}

Render 阶段对应 renderRoot: 循环调用 performUnitOfWork, 过程为深度优先遍历, 分为 两个阶段。 对应 BeginWork 阶段, 对应 CompleteWork 阶段。

五、BeginWork


5.1 认识

beginWork 阶段: 是向下调和的过程。从根节点 rootFiber 开始, 一直遍历到叶子节点,每次遍历到的节点都会传入当前 Fiber 节点, 执行beginWork。根据不同的节点类型, 调用不同的函数, 得到最新的 React Element, 对比(Diff) current fiberNodeReact Element, 然后创建或复用它的子Fiber节点, 标记对应的 flags, 并赋值给workInProgress.child生成 WorkInProgress fiberNode

  1. 进行 bailout 性能优化策略: 通过对比current树 与 workInProgressFiberpropscontextstate 等来命中 bailout 性能优化策略的组件可以不通过 reconcile 生成 workInProgress.child, 而是复用上次更新生成的 workInProgress.child, 减少不必要的子组件 render

  2. 根据 fiber.tag, 进行处理:

    1. HostRoot: 计算状态的最新值, 创建子 fiberNode

    2. ClassComponent: 计算 Update 状态, 执行类组件 render

    3. FunctionComponent: 调用 renderWithHooks, 执行函数组件, 得到最新的 ReactElement 对象, 创建子 fiberNode

render 阶段 React 并不会实质性的执行更新,而是只会给 fiber 打上不同的 flag 标志,证明当前 fiber 发生了什么变化, 并将这些变化冒泡到根节点。

5.2 计算

计算 Update 状态, 主要逻辑如下:

  1. 获取上次更新时因优先级较低而跳过组成的链表的头结点 firstBaseUpdate 与尾节点 lastBaseUpdate, 同时将 shared.pending 拆开拼接到尾节点 lastBaseUpdate 的后面

  2. 设置 newState 用于存储 Update 计算结果, 设置 newBaseState 用于存储第一个低优先级任务之前的 Update 计算结果 newState, 若不存在低优先级的任务,则 newBaseState 为执行完所有任务后得到的值 newState。设置 newLastBaseUpdate 用于存储低优先级的更新链表。

  3. 遍历 firstBaseUpdate 链表, 依次判断当前 Update 对应的任务优先级是否足够

  4. Update 优先级较低: 如果 newLastBaseUpdate 为空, 说明此时的 Update 是第一个低优先级任务, 更新 newBaseState 为上一次 Update 计算结果 newState。 如果 newLastBaseUpdate 不为空, 将低优先级任务加入到 newLastBaseUpdate 队列即可。

  5. Update 优先级足够: 如果存储低优先级任务链表的 newLastBaseUpdate 不为空, 为了保证操作的完整性, 将 Update 加入到 newLastBaseUpdate 后面。执行 Update, 获取最新计算结果 newState

  6. 最后, 将执行所有操作后得到的 newState 赋值给 fiber.memoizedState。将 newBaseState 赋值给 updateQueue.baseState, 作为下次的初始值。将 newLastBaseUpdate 赋值给 updateQueue.lastBaseUpdate, 用于下次更新。

六、CompleteWork


6.1 认识

completeWork 阶段: 是向上归并的过程。当 beginWork 阶段 遍历到叶子节点后, 开始向上归并, 执行 completeWork。根据不同的节点类型, 调用不同的函数, 处理 fiberprops(事件收集在这里进行)、向上冒泡标记flagssubtreeFlags, 进入不同的 DOM 节点创建、处理逻辑。执行完成之后会判断此节点的兄弟节点存不存在,如果存在就会为兄弟节点执行completeWork,当全部兄弟节点执行完之后,会向上冒泡到父节点执行completeWork,直到rootFiber

七、Commit


7.1 认识

commit 阶段: 此时的 WorkInProgress Fiber 树已经标记完 flagssubtreeFlags, 从 rooterFiber 开始, 递归遍历 fiberNode, 找到最底层的 fiber.subtreeFlags === subtreeFlags 的节点开始处理, 针对不同 flags 进行对应的处理。随后, 遍历兄弟节点, 最后遍历父节点。

7.2 执行钩子

commit 阶段执行的生命周期或者 effect 钩子是先子后父? 为什么呢? 本质上 commit 阶段处理的事情和 dom 元素有关系,commit 阶段生命周期是可以改变真实 dom 元素的状态的,所以如果在子组件生命周期内改变 dom 状态,并且想要在父组件的生命周期中同步状态,就需要确保父组件的生命周期执行时机要晚于子组件。