认识
一、认识
React.js
源码旨在了解、掌握 React
底层设计思想、实现。
二、编译
在 React.js 17
之前, 应用程序通过 @babel/preset-react
将 JSX
语法转换为 React.createElement
的 js
代码,因此需要显式将 React
引入,才能正常调用 createElement
。React.js 17
版本之后,官方与 babel
进行了合作,直接通过将 react/jsx-runtime
对 jsx
语法进行了新的转换而不依赖 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
渲染的根节点, 有 render
和 unmount
两个方法。主要工作是创建整个应用的 FiberRoot
, 创建一个 UpdateQueue
放入更新队列, 调用 scheduleUpdateOnFiber
进入调度更新流程, 开启并发模式, 开启自动批处理。初始化流程如下:
-
创建
root
应用根节点对象HostRootFiber
与FiberRootNode
, 然后关联两个对象-
FiberRootNode
: 是React
应用的根节点, 它的作用是负责应用加载相关的内容,比如应用加载模式mode
,存储本次应用更新的回调任务以及优先级,存储创建完成的FiberTree
等。 -
HostRootFiber
: 是虚拟DOM
树的根节点,类型是FiberNode
, 针对普通DOM
元素或者组件创建的Fiber
对象,是虚拟DOM
的真实体现
-
-
初始化
HostRootFiber.updateQueue
, 创建一个updateQueue
更新队列 -
给根元素绑定了所有事件,任何子孙元素触发的该类型事件都会委托给根元素的事件回调处理
-
以同步的优先级, 创建一个
update
对象,包含了更新的信息,将其添加到组件的更新队列中, 开启调度更新流程
四、Scheduler
4.1 更新队列
-
初始化
UpdateQueue
队列: 初始化一个UpdateQueue
,并将updateQueue
赋值给fiber.updateQueue
。updateQueue
队列是fiber
更新时要执行的内容。 -
创建
update
:update
保存更新状态的相关内容, 保存有对应的优先级Lane
-
进入
UpdateQueue
队列: 向链表fiber.updateQueue.shared.pending
中添加Update
节点, 形成一个单向环形链表。之所以设计为单向环形链表, 原因如下:-
做成环形链表可以只需要利用一个指针,便能找到头节点与尾节点
-
更加方便地找到最后一个
Update
对象,同时插入新的Update
对象也非常方便 -
如果使用普通的线性链表,就需要同时记录第一个和最后一个节点的位置,维护成本相对较高
-
4.2 调度更新
-
更新触发更新
Fiber
的优先级childLanes
, 递归向上把父级的childLanes
都更新,更新成当前的任务优先级, 直到rootFiber
, 并返回rootFiber
: 如果A
组件发生了更新,那么先向上递归更新父级链的childLanes
,接下来从Root Fiber
向下调和的时候,发现childLanes
等于当前更新优先级,那么说明它的child
链上有新的更新任务,则会继续向下调和,反之退出调和流程。因此, 通过RootFiber
是如何找到需要更新的组件的:Root Fiber
是通过对比每一级的childLanes
与当前更新优先级, 逐渐向下对比调和找到需要更新的组件的。 问题: 为什么要从触发更新的Fiber
递归向上形成一个childLanes
链, 而不可以从从触发的Fiber
直接向下调和呢?React
并不知道一次state
变化, 影响的范围, 比如一次更新发生, 父子Fiber
上都有更新, 如果直接向下调和, 那么是不合理的, 所以采用了从rootFiber
统一向下的方式。 -
标记本次更新任务优先级: 合并
rootFiber.childLanes
与当前更新任务lane
-
基于
rootFiber
开启调度更新 -
对比本次更新任务优先级和上一次等待更新任务优先级: 如果相等, 复用本次更新任务调度, 终止调度流程。如果不相等, 继续向下调度
-
根据本次更新任务的优先级,决定以同步还是并发的方式调度本次更新: 同步方式将
performSyncWorkOnRoot
同步任务加入到syncQueue
同步任务队列, 通过Promise.then
以微任务的方式, 遍历执行syncQueue
同步任务队列。异步方式通过Scheduler
中的scheduleCallback
以本次优先级对应的Scheduler
调度优先级, 调度执行performConcurrentWorkOnRoot
异步更新任务。 -
对于
scheduleCallback
中的异步任务: 在函数中, 不同的优先级意味着不同时长的任务过期时间。同时会比较startTime
与currentTime
, 如果startTime > currentTime
, 表示当前任务未就绪, 存入timerQueue
。并根据开始时间重新排列timerQueue
中任务的顺序。当timerQueue
中有任务就绪,即startTime <= currentTime
,我们将其取出并加入taskQueue
。timerQueue
和taskQueue
两个队列为了能在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 fiberNode
与 React Element
, 然后创建或复用它的子Fiber
节点, 标记对应的 flags
, 并赋值给workInProgress.child
生成 WorkInProgress fiberNode
。
-
进行
bailout
性能优化策略: 通过对比current
树 与workInProgress
树Fiber
的props
、context
、state
等来命中bailout
性能优化策略的组件可以不通过reconcile
生成workInProgress.child
, 而是复用上次更新生成的workInProgress.child
, 减少不必要的子组件render
。 -
根据
fiber.tag
, 进行处理:-
HostRoot
: 计算状态的最新值, 创建子fiberNode
-
ClassComponent
: 计算Update
状态, 执行类组件render
-
FunctionComponent
: 调用renderWithHooks
, 执行函数组件, 得到最新的ReactElement
对象, 创建子fiberNode
-
在 render
阶段 React
并不会实质性的执行更新,而是只会给 fiber
打上不同的 flag
标志,证明当前 fiber
发生了什么变化, 并将这些变化冒泡到根节点。
5.2 计算
计算 Update
状态, 主要逻辑如下:
-
获取上次更新时因优先级较低而跳过组成的链表的头结点
firstBaseUpdate
与尾节点lastBaseUpdate
, 同时将shared.pending
拆开拼接到尾节点lastBaseUpdate
的后面 -
设置
newState
用于存储Update
计算结果, 设置newBaseState
用于存储第一个低优先级任务之前的Update
计算结果newState
, 若不存在低优先级的任务,则newBaseState
为执行完所有任务后得到的值newState
。设置newLastBaseUpdate
用于存储低优先级的更新链表。 -
遍历
firstBaseUpdate
链表, 依次判断当前Update
对应的任务优先级是否足够 -
Update
优先级较低: 如果newLastBaseUpdate
为空, 说明此时的Update
是第一个低优先级任务, 更新newBaseState
为上一次Update
计算结果newState
。 如果newLastBaseUpdate
不为空, 将低优先级任务加入到newLastBaseUpdate
队列即可。 -
Update
优先级足够: 如果存储低优先级任务链表的newLastBaseUpdate
不为空, 为了保证操作的完整性, 将Update
加入到newLastBaseUpdate
后面。执行Update
, 获取最新计算结果newState
。 -
最后, 将执行所有操作后得到的
newState
赋值给fiber.memoizedState
。将newBaseState
赋值给updateQueue.baseState
, 作为下次的初始值。将newLastBaseUpdate
赋值给updateQueue.lastBaseUpdate
, 用于下次更新。
六、CompleteWork
6.1 认识
completeWork
阶段: 是向上归并的过程。当 beginWork
阶段 遍历到叶子节点后, 开始向上归并, 执行 completeWork
。根据不同的节点类型, 调用不同的函数, 处理 fiber
的 props
(事件收集在这里进行)、向上冒泡标记flags
、subtreeFlags
, 进入不同的 DOM
节点创建、处理逻辑。执行完成之后会判断此节点的兄弟节点存不存在,如果存在就会为兄弟节点执行completeWork
,当全部兄弟节点执行完之后,会向上冒泡到父节点执行completeWork
,直到rootFiber
。
七、Commit
7.1 认识
commit
阶段: 此时的 WorkInProgress Fiber
树已经标记完 flags
、subtreeFlags
, 从 rooterFiber
开始, 递归遍历 fiberNode
, 找到最底层的 fiber.subtreeFlags === subtreeFlags
的节点开始处理, 针对不同 flags
进行对应的处理。随后, 遍历兄弟节点, 最后遍历父节点。
7.2 执行钩子
commit
阶段执行的生命周期或者 effect
钩子是先子后父? 为什么呢? 本质上 commit
阶段处理的事情和 dom
元素有关系,commit
阶段生命周期是可以改变真实 dom
元素的状态的,所以如果在子组件生命周期内改变 dom
状态,并且想要在父组件的生命周期中同步状态,就需要确保父组件的生命周期执行时机要晚于子组件。