React 渲染全流程剖析:初次渲染与重渲染的底层逻辑
附上Vue渲染机制以作对比:Vue3渲染机制解析:编译时优化与虚拟DOM的性能跃迁
一、初次渲染
import Image from './Image.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Image />);
如以上代码初次渲染是调用createRoot
方法转入目标DOM节点,然后调用render
函数完成的渲染,以下是详细步骤:
1. JSX编译成虚拟DOM树
通过babel
编译转化jsx
为js
格式代码,如
return (
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
)
转化为
return (
React.createElement(
MyButton,
{color: 'blue', shadowSize: 2},
'Click Me'
)
)
createElement
接受以下参数:
- component:元素类型标签
- props:标签属性
- children:子节点
编译后的元素即是虚拟DOM树节点
注意:
render
函数在类组件中即是指render
方法,如果是在函数组件中则是函数组件本身,即
function Foo() {
return <h1> Foo </h1>;
}
2. Fiber树构建
创建根节点
function createFiberRoot(containerInfo) {
// 创建 FiberRoot(React应用根节点)
const root = new FiberRootNode(containerInfo);
// 创建未初始化的 HostRootFiber(Fiber树的根节点)
const uninitializedFiber = createHostRootFiber();
root.current = uninitializedFiber; // FiberRoot.current 指向 HostRootFiber
uninitializedFiber.stateNode = root; // HostRootFiber.stateNode 回指 FiberRoot
// 初始化更新队列
initializeUpdateQueue(uninitializedFiber);
return root;
}
创建更新对象
function updateContainer(element, container) {
const current = container.current; // 这里是HostRootFiber
const lane = requestUpdateLane(current); // 获取更新优先级
// 创建更新对象
const update = createUpdate(lane);
update.payload = { element }; // 存储 ReactElement (<App/>)
// - 将根组件 <App/> 存入 HostRootFiber 的更新队列
enqueueUpdate(current, update);
// 开始调度更新
scheduleUpdateOnFiber(current, lane);
}
// 更新后内存结构:
HostRootFiber.updateQueue.shared.pending = {
payload: { element: <App/> }, // 存储要渲染的组件
next: update // 环形链表
}
3. 协调阶段:深度优先构建Fiber树
-
workInProgress
指针跟踪当前处理的 Fiber - 此处无
alternate
指针,因为是初次渲染,无旧树进行比较
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate;
// 创建子Fiber节点
const next = beginWork(current, unitOfWork, renderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 完成当前节点
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next; // 继续处理子节点
}
}
例子1:
class App extends React.Component {
componentDidMount() {
console.log(`App Mount`);
console.log(`App 组件对应的fiber节点: `, this._reactInternals);
}
render() {
return (
<div className="app">
<header>header</header>
<Content />
</div>
);
}
}
class Content extends React.Component {
componentDidMount() {
console.log(`Content Mount`);
console.log(`Content 组件对应的fiber节点: `, this._reactInternals);
}
render() {
return (
<React.Fragment>
<p>1</p>
<p>2</p>
</React.Fragment>
);
}
}
export default App;
例子1中Fiber 树构建过程:
1. 从 HostRootFiber 开始,创建 <App>
Fiber 节点
2. 处理 <App>
的 render 结果,创建 <div>
Fiber 节点
3. 处理 <div>
的子节点,依次创建 <header>
和 <Content>
Fiber 节点
4. 处理 <Content>
的 render 结果,创建 <Fragment>
和两个 <p>
Fiber 节点
4. 完成阶段:创建DOM和收集副作用
- 所有标记了
Placement
的 Fiber 组成单向链表 - 顺序:深度优先,子节点在前,父节点在后
function completeWork(current, workInProgress) {
switch (workInProgress.tag) {
case HostComponent:
// 创建DOM实例
const instance = createInstance(workInProgress.type, workInProgress.pendingProps);
// 将子DOM附加到当前DOM
appendAllChildren(instance, workInProgress);
// 设置DOM属性
finalizeInitialChildren(instance, workInProgress.type, workInProgress.pendingProps);
workInProgress.stateNode = instance; // 关联DOM
// 标记插入操作
if (workInProgress.flags & Placement) {
markUpdate(workInProgress);
}
break;
case HostText:
// 文本节点处理...
// 其他类型...
}
// 收集副作用到父节点
if (workInProgress.flags > PerformedWork) {
appendEffectToList(workInProgress.return, workInProgress);
}
}
例子1的副作用链表的形成:<header>
→ <p>
1 → <p>
2 → <div>
→ <App>
→ HostRootFiber
5. 提交阶段:DOM操作
function commitRoot(root) {
const finishedWork = root.finishedWork;
let effect = finishedWork.firstEffect;
// 提交所有插入操作
while (effect !== null) {
commitPlacement(effect); // 实际DOM插入操作
effect = effect.nextEffect;
}
// 调用生命周期
commitLifeCycles(finishedWork);
}
二、重渲染
组件(或者是其祖先之一)的状态发生了改变会导致重新渲染。
1. 状态变更触发更新
创建更新对象
- 当组件状态变更(通过
setState
、useState
或forceUpdate
)时,React 会创建一个更新对象(update object
)。这个对象包含更新内容、优先级信息等,并添加到对应Fiber
节点的更新队列中 - 当使用
setState
时:
enqueueSetState(inst, payload) {
const fiber = getInstance(inst); // 获取组件对应fiber
const lane = requestUpdateLane(fiber); // 确定更新优先级
const update = createUpdate(eventTime, lane);
update.payload = payload; // 存储更新数据
// 将更新加入fiber的更新队列
enqueueUpdate(fiber, update);
// 开始调度
scheduleUpdateOnFiber(fiber, lane);
}
标记更新路径
- React 需要确定哪些节点会受到更新的影响,它会从触发更新的 Fiber 节点开始,向上遍历父节点,标记出整个更新路径
- 更新路径可帮助后续遍历跳过未标记的虚拟DOM子树
function markUpdateLaneFromFiberToRoot(sourceFiber, lane) {
// 设置当前fiber的lanes(标记当前节点需要更新)
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
// 向上遍历父节点,设置childLanes(标记子树需要更新)
let node = sourceFiber;
let parent = sourceFiber.return;
while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane);
node = parent;
parent = parent.return;
}
// 返回FiberRoot
return node.stateNode;
}
2. 准备阶段:双缓存结构初始化
- 使用双缓存技术,在更新时创建一棵新的
workInProgress
树,与current
树(当前显示树)交替使用
function prepareFreshStack(root, lanes) {
// 创建workInProgress树
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 初始化workInProgress节点
const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
workInProgressRoot = root;
workInProgressRootRenderLanes = lanes;
}
3. 协调阶段:对比更新
beginWork与节点对比
- 在 beginWork 阶段,React 对比新旧节点,决定是否需要更新或复用子树
function beginWork(current, workInProgress, renderLanes) {
if (current !== null) {
// 检查props是否变化
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
// 判断是否需要更新
if (oldProps === newProps && !hasLegacyContextChanged()) {
// 检查优先级:是否在本次渲染范围内
if (!includesSomeLane(renderLanes, workInProgress.lanes)) {
// 无需更新,进入bailout逻辑
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
didReceiveUpdate = true;
}
// 需要更新:调用具体updatexxx更新函数
switch (workInProgress.tag) {
case ClassComponent:
return updateClassComponent(current, workInProgress, ...);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
// 其他类型...
}
}
bailout 逻辑:跳过未变更子树
- 当节点不需要更新时,React进入bailout逻辑
- bailout 条件:props 未变化且优先级不匹配
- 根据更新路径可以跳过整个子树的重渲染
function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {
// 检查子节点是否需要更新
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// 整个子树都不需要更新,直接跳过
return null;
}
// 克隆子节点(复用现有 Fiber)
cloneChildFibers(current, workInProgress);
// 返回第一个子节点继续处理
return workInProgress.child;
}
4. updatexxx
函数与reconcileChildren
调和函数(此时需要更新)
当节点需要更新时,React 会调用调和函数,其实现的 Diff 算法会对比新旧子节点,决定复用、移动或删除节点。
传统的Diff算法是循环递归每一个节点(真实DOM节点),算法复杂度是O(n^3)。Vue和React都使用虚拟DOM节点的Diff算法,虚拟DOM是将目标所需的UI通过数据结构虚拟表现出来,保存到内存中,再将真实DOM与之保持同步。
Vue2的Diff算法使用双端对比虚拟节点,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作;Vue3在从=此基础借鉴了 ivi算法和 inferno算法。在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型。
React的Diff算法遵循以下三个层级的策略:tree Diff(只比较同层级节点,不跨层级比较)、component Diff(相同类型组件复用实例)、element Diff(使用 key 标识稳定元素)。本节不对diff算法展示细说,后面会出一篇新文章diff算法,到时候附上链接。
本节只需要了解reconcileChildren
调和函数:
- 调和函数是
updateXXX
(如: updateHostRoot, updateClassComponent 等)函数中的一项重要逻辑, 它的作用是向下生成子节点, 并设置fiber.flags
. 与初次渲染对比: - 初次创建时
fiber
节点没有比较对象, 所以在向下生成子节点的时候没有任何多余的逻辑 - 对比更新时需要把
ReactElement
对象与旧fiber
对象进行比较, 来判断是否需要复用旧fiber
对象. 调和函数的目的:
- 给新增,移动,和删除节点设置
fiber.flags
,flags标记:-
Placement
:新增或移动节点 -
Update
:更新属性 -
Deletion
:删除节点
-
- 如果是需要删除的
fiber
, 除了自身打上Deletion
之外, 还要将其添加到父节点的effects
链表中(正常副作用队列的处理是在completeWork
函数, 但是该节点(被删除)会脱离fiber
树, 不会再进入completeWork
阶段, 所以在beginWork
阶段提前加入副作用队列)
5. 完成阶段:completeWork
与副作用收集
在 completeWork 阶段,React 完成节点处理并收集副作用(即DOM 操作)。
completeWork函数与初次更新时的completeWork函数逻辑一致,只是此时current
不为null
function completeWork(current, workInProgress) {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case HostComponent: // DOM 元素
if (current !== null && workInProgress.stateNode != null) {
// 对比更新:比较新旧属性
updateHostComponent(current, workInProgress, newProps);
} else {
// 新增节点:创建 DOM 实例
const instance = createInstance(
workInProgress.type,
newProps,
workInProgress
);
// 关联 DOM 与 Fiber
workInProgress.stateNode = instance;
// 设置初始属性
finalizeInitialChildren(instance, newProps);
}
break;
// 其他类型处理...
}
// 收集副作用(flags)
if (workInProgress.flags > PerformedWork) {
// 添加到父节点的副作用链表
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = workInProgress;
} else {
returnFiber.lastEffect.nextEffect = workInProgress;
}
returnFiber.lastEffect = workInProgress;
}
}
触发updateHostComponent函数更新DOM属性
function updateHostComponent(current, workInProgress, newProps) {
const oldProps = current.memoizedProps;
// 比较新旧属性差异
if (oldProps !== newProps) {
// 计算属性差异
const updatePayload = prepareUpdate(
workInProgress.stateNode,
workInProgress.type,
oldProps,
newProps
);
// 存储差异到 updateQueue
workInProgress.updateQueue = updatePayload;
// 标记 Update 副作用
if (updatePayload) {
workInProgress.flags |= Update;
}
}
}
6. 提交阶段:DOM 操作与生命周期
提交阶段遍历副作用链表,执行 DOM 操作并调用生命周期方法
function commitRoot(root) {
const finishedWork = root.finishedWork;
let effect = finishedWork.firstEffect;
// 阶段1: BeforeMutation(调用 getSnapshotBeforeUpdate 获取 DOM 快照)
commitBeforeMutationEffects();
// 阶段2: Mutation(DOM操作)
while (effect !== null) {
const nextEffect = effect.nextEffect;
const flags = effect.flags;
// 处理 Placement(插入/移动)
if (flags & Placement) {
commitPlacement(effect);
}
// 处理 Update(属性更新)
else if (flags & Update) {
commitWork(effect);
}
// 处理 Deletion(删除)
else if (flags & Deletion) {
commitDeletion(effect);
}
effect = nextEffect;
}
// 阶段3: Layout(同步生命周期)
effect = finishedWork.firstEffect;
while (effect !== null) {
const nextEffect = effect.nextEffect;
if (effect.flags & Update) {
// 类组件:componentDidMount/Update
// 函数组件:useLayoutEffect
commitLayoutEffects(effect);
}
effect = nextEffect;
}
// 阶段4: Passive(异步 useEffect)
scheduleCallback(NormalPriority, () => {
flushPassiveEffects();
});
}
7. 清理与切换:完成更新
提交完成后,React 清理临时状态并切换 current 指针
function finishCommit() {
// 清理临时状态
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 切换 current 树,使新树成为当前树
root.current = finishedWork;
// 调度未处理的更新
if (root.pendingPassiveEffects !== null) {
scheduleCallback(NormalPriority, () => {
flushPassiveEffects();
return null;
});
}
// 检查是否有待处理的更新
ensureRootIsScheduled(root);
}
三、初次缓存和重缓存的区别
- 对于初次渲染, React 会调用根组件。
-
对于重渲染, React 会调用内部状态更新触发了渲染的函数组件
- 这个过程是递归的:如果更新后的组件会返回某个另外的组件,那么 React 接下来就会渲染 那个 组件,而如果那个组件又返回了某个组件,那么 React 接下来就会渲染 那个 组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。
特性 | 初次渲染 | 重渲染 |
---|---|---|
缓存机制 | 无缓存 | 双缓存树(current/workInProgress) |
节点创建 | 全量新建节点 | 复用节点 + 条件创建新节点 |
对比机制 | 无旧节点比较 | Fiber节点Diff算法(复用决策) |
副作用标记 | 全部标记为Placement
|
动态标记Update/Placement/Deletion
|
子树处理 | 全量处理 | bailout机制跳过未变更子树 |
DOM操作 | 全量插入 | 增量更新(仅变更部分) |
性能优化 | 无优化空间 | 优先级调度 + 子树跳过 |
alternate指针 | 不存在 | 新旧节点互相引用 |
四、React和Vue的渲染机制的区别
1. 虚拟DOM
- React:通过JSX编译生成虚拟DOM树,所有UI逻辑(插值、循环、条件)均用原生JavaScript实现。状态变化时生成新虚拟DOM树,通过Diff算法 计算最小变更集,再更新真实DOM
- Vue:模板编译为虚拟DOM树,但依赖响应式系统追踪数据变化。数据变更时直接定位受影响组件,生成局部虚拟DOM并比对,减少不必要的树遍历
2. 响应式系统
-
React:无自动依赖追踪。状态更新(
setState
/useState
)默认触发当前组件及所有子组件的重渲染,需开发者手动优化(如React.memo
或shouldComponentUpdate
) -
Vue:依赖收集+发布订阅
- Vue2:使用
Object.defineProperty
拦截数据读写 - Vue3:换成使用
Proxy
实现细粒度依赖追踪,数据变更时仅触发关联组件的更新
- Vue2:使用