自己动手写 React源码 ——【6】实现 Commit 阶段

自己动手写 React源码 ——【6】实现 Commit 阶段

深入理解 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)的 beginWorkcompleteWork 函数,接下来我们会实现提交阶段(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.tsrenderRoot 函数中,执行 commitRoot 函数。

  • commitRoot 是开始提交阶段的入口函数,调用 commitWork 函数进行实际的 DOM 操作;
  • commitWork 函数是提交阶段的核心,它会判断根节点是否存在上述 3 个阶段需要执行的操作,并执行实际的 DOM 操作,并完成 Fiber 树的切换。

我们先只实现 Mutation 阶段的功能,目前已支持的 DOM 操作有:Placement | Update | ChildDeletion,判断根节点的 flagssubtreeFlags 中是否包含这三个操作,如果有,则调用 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前端团队

(0)
上一篇 2024年3月20日 上午11:17
下一篇 2024年3月20日 下午4:05

相关推荐

发表回复

登录后才能评论