React18系列reconciler从0实现过程详解
作者:sunnyhuang519626
引言
本系列是讲述从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实现的资料请关注脚本之家其它相关文章!