深入理解 React 源码,带你从零实现 React v18 的核心功能,构建自己的 React 库。
电子书地址:2xiao.github.io/leetcode-js…
源代码地址:github.com/2xiao/my-re…
送我一个免费的 ⭐️ Star,这是对我最大的鼓励和支持。
在 第 4 节 中,我们提到 React 更新流程有四个阶段:
- 触发更新(Update Trigger)
- 调度阶段(Schedule Phase)
- 协调阶段(Reconciliation Phase)
- 提交阶段(Commit Phase)
之前我们已经实现了协调阶段(Reconciliation Phase)的 beginWork
和 completeWork
函数,接下来我们会实现提交阶段(Commit Phase)。
提交阶段的主要任务是将更新同步到实际的 DOM 中,执行 DOM 操作,例如创建、更新或删除 DOM 元素,反映组件树的最新状态,可以分为三个主要的子阶段:
-
Before Mutation (布局阶段):
主要用于执行 DOM 操作之前的准备工作,包括类似 getSnapshotBeforeUpdate 生命周期函数的处理。在这个阶段会保存当前的布局信息,以便在后续的 DOM 操作中能够进行比较和优化。
-
Mutation (DOM 操作阶段):
执行实际 DOM 操作的阶段,包括创建、更新或删除 DOM 元素等。使用深度优先遍历的方式,逐个处理 Fiber 树中的节点,根据协调阶段生成的更新计划,执行相应的 DOM 操作。
-
Layout (布局阶段):
用于处理布局相关的任务,进行一些布局的优化,比如批量更新布局信息,减少浏览器的重排(reflow)次数,提高性能。其目标是最小化浏览器对 DOM 的重新计算布局,从而提高渲染性能。
1. 实现 commitWork
首先,在 react-reconciler/src/workLoop.ts
的 renderRoot
函数中,执行 commitRoot
函数。
commitRoot
是开始提交阶段的入口函数,调用commitWork
函数进行实际的 DOM 操作;commitWork
函数是提交阶段的核心,它会判断根节点是否存在上述 3 个阶段需要执行的操作,并执行实际的 DOM 操作,并完成 Fiber 树的切换。
我们先只实现 Mutation 阶段的功能,目前已支持的 DOM 操作有:Placement | Update | ChildDeletion
,判断根节点的 flags
和 subtreeFlags
中是否包含这三个操作,如果有,则调用 commitMutationEffects
函数执行实际的 DOM 操作。
需要注意的是,由于 current
是与视图中真实 UI 对应的 Fiber 树,而 workInProgress
是触发更新后正在 Reconciler 中计算的 Fiber 树,因此在 DOM 操作执行完之后,需要将 current
指向 workInProgress
,完成 Fiber 树的切换。
// packages/react-reconciler/src/workLoop.ts
import { MutationMask, NoFlags } from './fiberFlags';
import { commitMutationEffects } from './commitWork';
// ...
function renderRoot(root: FiberRootNode) {
// 初始化 workInProgress 变量
prepareFreshStack(root);
do {
try {
// 深度优先遍历
workLoop();
break;
} catch (e) {
console.warn('workLoop发生错误:', e);
workInProgress = null;
}
} while (true);
// 创建根 Fiber 树的 Root Fiber
const finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
// 提交阶段的入口函数
commitRoot(root);
}
function commitRoot(root: FiberRootNode) {
const finishedWork = root.finishedWork;
if (finishedWork === null) {
return;
}
if (__DEV__) {
console.log('commit 阶段开始');
}
// 重置
root.finishedWork = null;
// 判断是否存在 3 个子阶段需要执行的操作
const subtreeHasEffects =
(finishedWork.subtreeFlags & MutationMask) !== NoFlags;
const rootHasEffects = (finishedWork.flags & MutationMask) !== NoFlags;
if (subtreeHasEffects || rootHasEffects) {
// TODO: BeforeMutation
// Mutation
commitMutationEffects(finishedWork);
// Fiber 树切换,workInProgress 变成 current
root.current = finishedWork;
// TODO: Layout
} else {
root.current = finishedWork;
}
}
2. 实现 Mutation
接下来我们来实现 Mutation 阶段执行 DOM 操作的具体实现,新建 packages/react-reconciler/src/commitWork.ts
文件,定义 commitMutationEffects
函数。
commitMutationEffects
函数负责深度优先遍历 Fiber 树,递归地向下寻找子节点是否存在 Mutation 阶段需要执行的 flags,如果遍历到某个节点,其所有子节点都不存在 flags(即 subtreeFlags == NoFlags
),则停止向下,调用 commitMutationEffectsOnFiber
处理该节点的 flags,并且开始遍历其兄弟节点和父节点。
commitMutationEffectsOnFiber
会根据每个节点的 flags 和更新计划中的信息执行相应的 DOM 操作。
以 Placement
为例:如果 Fiber 节点的标志中包含 Placement
,表示需要在 DOM 中插入新元素,此时就需要取到该 Fiber 节点对应的 DOM,并将其插入对应的父 DOM 节点中。
// packages/react-reconciler/src/commitWork.ts
import { Container, appendChildToContainer } from 'hostConfig';
import { FiberNode, FiberRootNode } from './fiber';
import {
ChildDeletion,
MutationMask,
NoFlags,
Placement,
Update
} from './fiberFlags';
import { HostComponent, HostRoot, HostText } from './workTags';
let nextEffect: FiberNode | null = null;
export const commitMutationEffects = (finishedWork: FiberNode) => {
nextEffect = finishedWork;
// 深度优先遍历 Fiber 树,寻找更新 flags
while (nextEffect !== null) {
// 向下遍历
const child: FiberNode | null = nextEffect.child;
if (
(nextEffect.subtreeFlags & MutationMask) !== NoFlags &&
child !== null
) {
// 子节点存在 mutation 阶段需要执行的 flags
nextEffect = child;
} else {
// 子节点不存在 mutation 阶段需要执行的 flags 或没有子节点
// 向上遍历
up: while (nextEffect !== null) {
// 处理 flags
commitMutationEffectsOnFiber(nextEffect);
const sibling: FiberNode | null = nextEffect.sibling;
// 遍历兄弟节点
if (sibling !== null) {
nextEffect = sibling;
break up;
}
// 遍历父节点
nextEffect = nextEffect.return;
}
}
}
};
const commitMutationEffectsOnFiber = (finishedWork: FiberNode) => {
const flags = finishedWork.flags;
if ((flags & Placement) !== NoFlags) {
commitPlacement(finishedWork);
finishedWork.flags &= ~Placement;
}
if ((flags & Update) !== NoFlags) {
// TODO Update
finishedWork.flags &= ~Update;
}
if ((flags & ChildDeletion) !== NoFlags) {
// TODO ChildDeletion
finishedWork.flags &= ~ChildDeletion;
}
};
// 执行 DOM 插入操作,将 FiberNode 对应的 DOM 插入 parent DOM 中
const commitPlacement = (finishedWork: FiberNode) => {
if (__DEV__) {
console.log('执行 Placement 操作', finishedWork);
}
const hostParent = getHostParent(finishedWork);
if (hostParent !== null) {
appendPlacementNodeIntoContainer(finishedWork, hostParent);
}
};
// 获取 parent DOM
const getHostParent = (fiber: FiberNode): Container | null => {
let parent = fiber.return;
while (parent !== null) {
const parentTag = parent.tag;
// 处理 Root 节点
if (parentTag === HostRoot) {
return (parent.stateNode as FiberRootNode).container;
}
// 处理原生 DOM 元素节点
if (parentTag === HostComponent) {
return parent.stateNode as Container;
} else {
parent = parent.return;
}
}
if (__DEV__) {
console.warn('未找到 host parent', fiber);
}
return null;
};
const appendPlacementNodeIntoContainer = (
finishedWork: FiberNode,
hostParent: Container
) => {
if (finishedWork.tag === HostComponent || finishedWork.tag === HostText) {
appendChildToContainer(finishedWork.stateNode, hostParent);
} else {
const child = finishedWork.child;
if (child !== null) {
appendPlacementNodeIntoContainer(child, hostParent);
let sibling = child.sibling;
while (sibling !== null) {
appendPlacementNodeIntoContainer(sibling, hostParent);
sibling = sibling.sibling;
}
}
}
};
至此,我们就完成了 React 更新流程中的提交阶段(Commit Phase),实现了 DOM 树更新,下一节我们将实现 react-dom 包,跑通整个 React 首屏渲染流程。
相关代码可在 git tag v1.6
查看,地址:github.com/2xiao/my-re…
《自己动手写 React 源码》遵循 React 源码的核心思想,通俗易懂的解析 React 源码,带你从零实现 React v18 的核心功能。
学完本书,你将有这些收获:
面试加分:框架底层原理是面试必问环节,熟悉 React 源码会为你的面试加分,也会为你拿下 offer 增加不少筹码;
提升开发效率:熟悉 React 源码之后,会对 React 的运行流程有新的认识,让你在日常的开发中,对性能优化、使用技巧和 bug 解决更加得心应手;
巩固基础知识:学习本书也顺便巩固了数据结构和算法,如 reconciler 中使用了 fiber、update、链表等数据结构,diff 算法要考虑怎样降低对比复杂度;
本书的特色:
教程详细,代码开源,带你构建自己的 React 库;
功能全面,可跑通官方测试用例;
按 Git Tag 划分迭代步骤,记录每个功能的实现过程;
电子书地址:2xiao.github.io/leetcode-js…
源代码地址:github.com/2xiao/my-re…
送我一个免费的 ⭐️ Star,这是对我最大的鼓励和支持。
原文链接:https://juejin.cn/post/7348264380259876899 作者:腾讯TNTWeb前端团队