引言
本系列是讲述从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元素。
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
指向了这个指针
export const createUpdateQueue = <State>() => {
return {
shared: {
pending: null,
},
} as UpdateQueue<State>;
};
我们从上面createRoot
执行完后,返回了一个render函数,我们接下来看看render后的执行过程,是怎么渲染到页面的。
render() 调用
createRoot
执行后,创建了一个rootFiberNode
, 并返回了render
调用,主要是执行了updateContainer
用于去渲染初始化的工作。
updateContainer
接受2个参数,第一个参数是传入的ReactElement
(), 第二个参数是fiberRootNode
。
主要是做3件事情:
- 创建一个更新事件
- 把更新事件推进队列中
- 调用调度,开始更新
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对象
export const createUpdate = (action) => {
return {
action,
};
};
将更新推进队列enqueueUpdate
接受2个参数,第一个参数是我们创建一个更新队列的引用,第二个是新增的队列
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
分支
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
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
是打标记使用的,我们暂时先不研究。
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树如下图。
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元素的层级可能不是一一对应的。
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实现的资料请关注编程网其它相关文章!