引言
本系列是讲述从0开始实现一个react18的基本版本。由于React源码通过Mono-repo 管理仓库,我们也是用pnpm提供的workspaces来管理我们的代码仓库,打包我们使用rollup进行打包。
我们这一次主要写有关调和(reconciler)和ReactDom,React将调和单独的抽出一个包,暴露出入口,通过不同的宿主环境去调用不同的api。
React-Dom包
这个包主要是提供浏览器环境的一些dom操作。主要是提供2个文件hostConfig.ts 以及root.ts。 想想我们在React18中,是通过如下方式调用的。所以我们需要提供一个方法createRoot方法,返回要给包含render函数的对象。
import ReactDOM from 'react-dom/client'; ReactDOM.createRoot(root).render(<App />)
createRoot
主要功能是2个,第一个是创建根fiberNode节点, 第二个创建更新(初始化主要是用于渲染),开始调度。
//createRoot.ts 文件
import {
createContainer,
updateContainer,
} from "../../react-reconciler/src/filerReconciler";
export function createRoot(container: Container) {
const root = createContainer(container);
return {
render(element: ReactElementType) {
updateContainer(element, root);
},
};
}
createRoot.js主要是调用的react-reconciler的createContainer方法和updateContainer方法。我们之后看看这2个方法主要的作用
hostConfig.ts
主要是创建各种dom,已经dom的插入操作
export const createInstance = (type: string, props: any): Instance => {
// TODO 处理props
const element = document.createElement(type);
return element;
};
export const appendInitialChild = (
parent: Instance | Container,
child: Instance
) => {
parent.appendChild(child);
};
export const createTextInstance = (content: string) => {
return document.createTextNode(content);
};
export const appendChildToContainer = appendInitialChild;
React-reconciler包
createContainer() 函数
从上面我们可以知道,首先调用的createContainer和updateContainer,我们把它写到filerReconciler.ts中createContainer接受传入的dom元素。
/**
* ReactDOM.createRoot()中调用
* 1. 创建fiberRootNode 和 hostRootFiber。并建立联系
* @param {Container} container
*/
export function createContainer(container: Container) {
const hostRootFiber = new FiberNode(HostRoot, {}, null);
const fiberRootNode = new FiberRootNode(container, hostRootFiber);
hostRootFiber.updateQueue = createUpdateQueue();
return fiberRootNode;
}
可以看到我们在这里主要就是2个事情
调用了2个方法去创建2个不同的fiberNode,一个是hostRootFiber,一个是fiberRootNode
创建一个更新队列,并将其赋值给hostRootFiber
/**
* 顶部节点
*/
export class FiberRootNode {
container: Container; // 不同环境的不同的节点 在浏览器环境 就是 root节点
current: FiberNode;
finishedWork: FiberNode | null; // 递归完成后的hostRootFiber
constructor(container: Container, hostRootFiber: FiberNode) {
this.container = container;
this.current = hostRootFiber;
hostRootFiber.stateNode = this;
this.finishedWork = null;
}
}
export class FiberNode {
constructor(tag: WorkTag, pendingProps: Props, key: Key) {
this.tag = tag;
this.pendingProps = pendingProps;
this.key = key;
this.stateNode = null; // dom引用
this.type = null; // 组件本身 FunctionComponent () => {}
// 树状结构
this.return = null; // 指向父fiberNode
this.sibling = null; // 兄弟节点
this.child = null; // 子节点
this.index = 0; // 兄弟节点的索引
this.ref = null;
// 工作单元
this.pendingProps = pendingProps; // 等待更新的属性
this.memoizedProps = null; // 正在工作的属性
this.memoizedState = null;
this.updateQueue = null;
this.alternate = null; // 双缓存树指向(workInProgress 和 current切换)
this.flags = NoFlags; // 副作用标识
this.subtreeFlags = NoFlags; // 子树中的副作用
}
}

接下来,我们看看createUpdateQueue里面的执行逻辑。执行了一个函数,返回了一个对象。所以现在hostRootFiber的updateQueue指向了这个指针
/**
* 初始化updateQueue
* @returns {UpdateQueue<Action>}
*/
export const createUpdateQueue = <State>() => {
return {
shared: {
pending: null,
},
} as UpdateQueue<State>;
};
我们从上面createRoot执行完后,返回了一个render函数,我们接下来看看render后的执行过程,是怎么渲染到页面的。
render() 调用
createRoot执行后,创建了一个rootFiberNode, 并返回了render调用,主要是执行了updateContainer用于去渲染初始化的工作。
updateContainer接受2个参数,第一个参数是传入的ReactElement(), 第二个参数是fiberRootNode。
主要是做3件事情:
- 创建一个更新事件
- 把更新事件推进队列中
- 调用调度,开始更新
/**
* ReactDOM.createRoot().render 中调用更新
* 1. 创建update, 并将其推到enqueueUpdate中
*/
export function updateContainer(
element: ReactElementType | null,
root: FiberRootNode
) {
const hostRootFiber = root.current;
const update = createUpdate<ReactElementType | null>(element);
enqueueUpdate(
hostRootFiber.updateQueue as UpdateQueue<ReactElementType | null>,
update
);
// 插入更新后,进入调度
scheduleUpdateOnFiber(hostRootFiber);
return element;
}
创建更新createUpdate
实际上就是创建一个对象,由于初始化的时候传入的是ReactElementType(), 所以返回的是App对应的ReactElement对象
/**
* 创建更新
* @param {Action<State>} action
* @returns {Update<State>}
*/
export const createUpdate = (action) => {
return {
action,
};
};
将更新推进队列enqueueUpdate
接受2个参数,第一个参数是我们创建一个更新队列的引用,第二个是新增的队列
/**
* 更新update
* @param {UpdateQueue<Action>} updateQueue
* @param {Update<Action>} update
*/
export const enqueueUpdate = <State>(
updateQueue: UpdateQueue<State>,
update: Update<State>
) => {
updateQueue.shared.pending = update;
};
执行到这一步骤,我们得到了更新队列,其实是一个ReactElement组件 及我们调用render传入的jsx对象。
开始调用scheduleUpdateOnFiber
接受FiberNode开始执行我们的渲染工作, 一开始渲染传入的是hostFiberNode 之后其他更新传递的是对应的fiberNode
export function scheduleUpdateOnFiber(fiber: FiberNode) {
// todo 调度功能
let root = markUpdateFromFiberToRoot(fiber);
renderRoot(root);
}
wookLoop
执行完上面的操作后,接下来进入的调和阶段。开始我们要明白一个关键词:
workInProgress: 表示当前正在调和的fiber节点,之后简称wip
beginWork: 主要是根据当前fiberNode创建下一级fiberNode,在update时标记placement(新增、移动)ChildDeletion(删除)
completeWork: 在mount时构建Dom Tree, 初始化属性,在Update时标记Update(属性更新),最终执行flags冒泡
flags冒泡我们下一节讲。
从上面我们可以看到调用了scheduleUpdateOnFiber方法,开始从根部渲染页面。scheduleUpdateOnFiber主要是执行了2个方法:
markUpdateFromFiberToRoot: 由于我们更新的节点可能不是hostfiberNode, 这个方法就是不管传入的是那个节点,返回我们的根节点rootFiberNode
// 从当前触发更新的fiber向上遍历到根节点fiber
function markUpdateFromFiberToRoot(fiber: FiberNode) {
let node = fiber;
let parent = node.return;
while (parent !== null) {
node = parent;
parent = node.return;
}
if (node.tag === HostRoot) {
return node.stateNode;
}
return null;
}
renderRoot: 这里是我们wookLoop的入口,也是调和完成后,将生成的fiberNode树,赋值给finishedWork,并挂在根节点上,进入commit的入口。
function renderRoot(root: FiberRootNode) {
// 初始化,将workInProgress 指向第一个fiberNode
prepareFreshStack(root);
do {
try {
workLoop();
break;
} catch (e) {
if (__DEV__) {
console.warn("workLoop发生错误", e);
}
workInProgress = null;
}
} while (true);
const finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
// wip fiberNode树 树中的flags执行对应的操作
commitRoot(root);
}
prepareFreshStack函数: 用于初始化当前节点的wip, 并创建alternate 的双缓存的建立。 由于我们开始的时候传入的hostFiberNode, 经过createWorkInProgress后,创建了一个新的fiberNode 并通过alternate相互指向。并赋值给wip
let workInProgress: FiberNode | null = null;
function prepareFreshStack(root: FiberRootNode) {
workInProgress = createWorkInProgress(root.current, {});
}
export const createWorkInProgress = (
current: FiberNode,
pendingProps: Props
): FiberNode => {
let wip = current.alternate;
if (wip === null) {
//mount
wip = new FiberNode(current.tag, pendingProps, current.key);
wip.stateNode = current.stateNode;
wip.alternate = current;
current.alternate = wip;
} else {
//update
wip.pendingProps = pendingProps;
// 清掉副作用(上一次更新遗留下来的)
wip.flags = NoFlags;
wip.subtreeFlags = NoFlags;
}
wip.type = current.type;
wip.updateQueue = current.updateQueue;
wip.child = current.child;
wip.memoizedProps = current.memoizedProps;
wip.memoizedState = current.memoizedState;
return wip;
};

接下来我们来分析一下workLoop中到底是如何生成fiberNode树的。它本身函数执行很简单。就是不停的根据wip进行单个fiberNode的处理。 此时wip指向的hostRootFiber。开始执行performUnitOfWork进行递归操作,其中递:beginWork,归:completeWork。React通过DFS,首先找到对应的叶子节点。
function workLoop() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(fiber: FiberNode): void {
const next = beginWork(fiber); // next 是fiber的子fiber 或者 是null
// 工作完成,需要将pendingProps 复制给 已经渲染的props
fiber.memoizedProps = fiber.pendingProps;
if (next === null) {
// 没有子fiber
completeUnitOfWork(fiber);
} else {
workInProgress = next;
}
}
beginWork开始
主要是向下进行遍历,创建不同的fiberNode。由于我们传入的是HostRoot,所以会走到updateHostRoot分支
/**
* 递归中的递阶段
* 比较 然后返回子fiberNode 或者null
*/
export const beginWork = (wip: FiberNode) => {
switch (wip.tag) {
case HostRoot:
return updateHostRoot(wip);
case HostComponent:
return updateHostComponent(wip);
case HostText:
// 文本节点没有子节点,所以没有流程
return null;
default:
if (__DEV__) {
console.warn("beginWork未实现的类型");
}
break;
}
return null;
};
updateHostRoot
这个方法主要是2个部分:
- 根据我们之前创建的更新队列获取到最新的值
- 创建子fiber
/**
processUpdateQueue: 是根据不同的类型(函数和其他)生成memoizedState
*/
function updateHostRoot(wip: FiberNode) {
const baseState = wip.memoizedState;
const updateQueue = wip.updateQueue as UpdateQueue<ElementType>;
// 这里获取之前的更新队列
const pending = updateQueue.shared.pending;
updateQueue.shared.pending = null;
const { memoizedState } = processUpdateQueue(baseState, pending); // 最新状态
wip.memoizedState = memoizedState; // 其实就是传入的element
const nextChildren = wip.memoizedState; // 就是我们传入的ReactElement 对象
reconcileChildren(wip, nextChildren);
return wip.child;
}
reconcileChildren
调和子节点, 根据是否生成过,分别调用不同的方法。通过上面我们知道传入的hostFiber, 此时是存在alternate属性的,所以会走到reconcilerChildFibers分支。
根据当前传入的returnFiber是hostFiberNode以及currentFiber为null,newChild为ReactElementType。我们可以判断接下来会走到reconcileSingleElement的执行。其中placeSingleChild是打标记使用的,我们暂时先不研究。
/**
wip: 当前正在执行的父fiberNode
children: 即将要生成的子fiberNode
*/
function reconcileChildren(wip: FiberNode, children?: ReactElementType) {
const current = wip.alternate;
if (current !== null) {
// update
wip.child = reconcilerChildFibers(wip, current?.child, children);
} else {
// mount
wip.child = mountChildFibers(wip, null, children);
}
}
function reconcilerChildFibers(
returnFiber: FiberNode,
currentFiber: FiberNode | null,
newChild?: ReactElementType | string | number
) {
// 判断当前fiber的类型
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(returnFiber, currentFiber, newChild)
);
default:
if (__DEV__) {
console.warn("未实现的reconcile类型", newChild);
}
break;
}
}
// Todo 多节点的情况 ul > li * 3
// HostText
if (typeof newChild === "string" || typeof newChild === "number") {
return placeSingleChild(
reconcileSingleTextNode(returnFiber, currentFiber, newChild)
);
}
if (__DEV__) {
console.warn("未实现的reconcile类型", newChild);
}
return null;
};
}
reconcileSingleElement
从名字我们可以看出是通过ReactElement 创建单一的fiberNode。通过reconcileSingleElement我们就可以得出了一个新的子节点,然后通过return指向父fiber。此时的fiberNode树如下图。
/**
* 根据reactElement对象创建fiber并返回
*/
function reconcileSingleElement(
returnFiber: FiberNode,
_currentFiber: FiberNode | null,
element: ReactElementType
) {
const fiber = createFiberFromElement(element);
fiber.return = returnFiber;
return fiber;
}
export function createFiberFromElement(element: ReactElementType): FiberNode {
const { type, key, props } = element;
let fiberTag: WorkTag = FunctionComponent;
if (typeof type === "string") {
// <div/> type : 'div'
fiberTag = HostComponent;
} else if (typeof type !== "function" && __DEV__) {
console.log("未定义的type类型", element);
}
const fiber = new FiberNode(fiberTag, props, key);
fiber.type = type;
return fiber;
}

调用完后,此时回到了reconcileChildren函数的这一句代码执行,指定wip的child指向。此时函数执行完毕。
// 省略无关代码
function reconcileChildren(wip: FiberNode, children?: ReactElementType) {
wip.child = reconcilerChildFibers(wip, current?.child, children);
}

执行完后返回updateHostRoot函数调用reconcileChildren的地方。然后返回wip的child。
function updateHostRoot(wip) {
const baseState = wip.memoizedState;
reconcileChildren(wip, nextChildren);
return wip.child;
}
执行完updateHostRoot函数后,返回调用它的beginWork中。beginWork也同样返回了当前wip的child节点。
export const beginWork = (wip: FiberNode) => {
switch (wip.tag) {
case HostRoot:
return updateHostRoot(wip);
}
}
执行完后,我们最后又回到了最开始调用beginWork的地方。进行接下来的操作,主要是将已经渲染过的属性赋值。然后将wip赋值给下一个刚刚生成的子节点。以便于开始下一次的递归中调用。
function performUnitOfWork(fiber) {
const next = beginWork(fiber); // next 是fiber的子fiber 或者 是null
// 工作完成,需要将pendingProps 复制给 已经渲染的props
fiber.memoizedProps = fiber.pendingProps;
if (next === null) {
// 没有子fiber
completeUnitOfWork(fiber);
}
else {
workInProgress = next;
}
}
由于workInProgress不等于null, 说明还有子节点。继续进行workLoop调用。又开始了新的一轮。直到我们到达了叶子节点。
function workLoop() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
例子
例如,如下例子,当遍历到hcc文本节点后,由于我们节点是没有调和流程的。所以执行到beginWork后,返回了一个null。正式结束了递归调用中的“递" 过程。此时的fiberNode树如下图所示。
const jsx = <div><span>hcc</span></div>
const root = document.querySelector('#root')
ReactDOM.createRoot(root).render(jsx)

completeWork开始
从上面的beginWork操作后,此时我们wip在文本节点hcc的节点位置.
completeUnitOfWork
接下来执行performUnitOfWork中的completeUnitOfWork的逻辑部分,我们看看completeUnitOfWork的逻辑部分。 我们传入的最底部的叶子节点。首先会对当前节点进行completeWork的方法调用。
function completeUnitOfWork(fiber) {
let node = fiber;
do {
completeWork(node);
const sibling = node.sibling;
if (sibling !== null) {
workInProgress = sibling;
return;
}
node = node.return;
workInProgress = node;
} while (node !== null);
}
completeWork
首次我们会接受到一个最底部的子fiberNode,由于是第一次mount,所以当前的fiber下不会存在alternate属性的,所以会走到构建Dom的流程。
/**
* 递归中的归
*/
export const completeWork = (wip: FiberNode) => {
const newProps = wip.pendingProps;
const current = wip.alternate;
switch (wip.tag) {
case HostComponent:
if (current !== null && wip.stateNode) {
//update
} else {
// 1. 构建DOM
const instance = createInstance(wip.type, newProps);
// 2. 将DOM插入到DOM树中
appendAllChildren(instance, wip);
wip.stateNode = instance;
}
bubbleProperties(wip);
return null;
case HostText:
if (current !== null && wip.stateNode) {
//update
} else {
// 1. 构建DOM
const instance = createTextInstance(newProps.content);
// 2. 将DOM插入到DOM树中
wip.stateNode = instance;
}
bubbleProperties(wip);
return null;
case HostRoot:
bubbleProperties(wip);
return null;
default:
if (__DEV__) {
console.warn("未实现的completeWork");
}
break;
}
};
// 根据逻辑判断,走到下面的逻辑判断,传入了文本
// 1. 构建DOM
const instance = createTextInstance(newProps.content);
// 2. 将DOM插入到DOM树中
wip.stateNode = instance;
经过completeWork后,我们给当前的wip添加了stateNode属性,用于指向生成的Dom节点。 执行完completeWork后,继续返回到completeUnitOfWork中,查找sibling节点,目前我们demo中没有,所以会向上找到当前节点的return指向。继续执行completeWork工作,此时的结构变成了如下图:

由于我们wip目前是HostComponent, 所以走到了如下的completeWork的逻辑。这里 根据type创建不同的Dom元素,和之前一样,绑定到对应的stateNode属性上。我们可以看到除了这2个,还执行了一个函数appendAllChildren。我们去看看这个函数的作用是什么
// 1. 构建DOM const instance = createInstance(wip.type); // 2. 将DOM插入到DOM树中 appendAllChildren(instance, wip); wip.stateNode = instance;
appendAllChildren
接受2个参数,第一个是刚刚通过wip的type生成的对应的dom, 另外一个是wip本身。 它的作用就是把我们上一步产生的dom节点,插入到刚刚产生的父dom节点上,形成一个局部的小dom树。
它本身存在一个复杂的遍历过程,因为fiberNode的层级和DOM元素的层级可能不是一一对应的。
/**
* 在parent的节点下,插入wip
* @param {FiberNode} parent
* @param {FiberNode} wip
*/
function appendAllChildren(parent: Container, wip: FiberNode) {
let node = wip.child;
while (node !== null) {
if (node?.tag === HostComponent || node?.tag === HostText) {
appendInitialChild(parent, node?.stateNode);
} else if (node.child !== null) {
node.child.return = node;
// 继续向下查找
node = node.child;
continue;
}
if (node === wip) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === wip) {
return;
}
// 向上找
node = node?.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}
我们用这个图来说明一下流程

- 当前的”归“到了
div对应的fiberNode。我们获取到node是第一个子元素的span, 执行appendInitialChild方法,把对应的stateNode的dom节点插入parent中。 - 接下来执行由于
node.sibling不为空,所以会将node 复制给第二个span。然后继续执行appendInitialChild。以此执行到第三个span节点。 - 第三个span节点对应的
sibling为空,所以开始向上查找到node.return === wip结束函数调用。 - 此时三个span产生的dom,都已经插入到
parent(div dom)中。
回到completeUnitOfWork
经过上述操作后,我们继续回到completeUnitOfWork的调用,继续向上归并。到上述例子的div节点。直到我们遍历到hostFiberNode, 它是没有return属性的,所以返回null,结束了completeUnitOfWork的执行。回到了最开始的workLoop。此时的workInProgress等于null, 结束循环。
function workLoop() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
回到renderRoot
执行完workLoop, 就回到了renderRoot的部分。此时我们已经得到了完整的fiberNode树,以及相应的dom元素。此时对应的结果如下图:

那么生成的fiberNode树是如何渲染的界面上的,我们下一章的commit章节介绍,如何打标签和渲染,更多关于React18系列reconciler实现的资料请关注好代码网其它相关文章!