react图解源码之初始化挂载
react内部的初始化挂载
现在下面有如下页面渲染代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>React in HTML</title>
<script src="./react.development.js"></script>
<script src="./react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<style>
.component {
border: 1px solid #ccc;
padding: 10px;
margin: 10px;
}
</style>
<body>
<div id="root"></div>
<!-- 使用 type="text/babel" 让 Babel 编译 JSX -->
<script type="text/babel">
function A() {
return (
<div className="component" data-name="A">
<div>A</div>
<div>B</div>
</div>
);
}
// 创建 React 组件
function App() {
return (
<A />
);
}
// 渲染组件到 DOM
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>
Fiber结构介绍
上面的代码大概是如下的层级结构:
<App>
<A>
<div>
<div>A</div>
<div>B</div>
</div>
</A>
</App>
上面的结构在React中会构建如下图的一个FiberTree
从上图中可以看到,在该FiberTree中一共包含2种不同的类型:
-
FiberRootNode:FiberRootNode是一个特殊节点,充当React的根节点,它保存着整个应用程序所需的元数据。其current属性指向实际的Fiber树结构,每次构建新的Fiber树时,它都会将current重新指向新的HostRoot。 -
FiberNode:react内部中对结点的一种表示,包含很多属性可以对其结点进行描述-
tag:FiberNode有许多不同的子类型,在render以及commit阶段会根据该值进行不同的处理,如HostRoot、FunctionComponent、ClassComponent、HostComponent等等 -
stateNode: 对于tag为HostComponet的FiberNode,其指向页面中实际渲染的DOM节点 -
child、sibling、return: 分别指向子节点,兄弟节点以及父节点,用于构造完整Fiber树 -
flags: 用于表示在commit阶段需要更新的类型。subtreeFlags 表示其子树需要更新的类型
-
上面对于FiberNode的属性介绍只包含当前初始化页面所需要的属性,其他属性在后面需要用到时再进行解释。
大致过程
我们先来把整个渲染过程进行一个粗略的介绍,我们在渲染页面时,会执行下面的代码
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
上面的代码中,分为了以下两个主要过程:
createRootrender
createRoot
上面的createRoot会创建一个FiberRootNode
function createRoot(container, options) {
var root = createContainer(container, ConcurrentRoot, null, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError);
return new ReactDOMRoot(root);
}
function createContainer(containerInfo, tag, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError, transitionCallbacks) {
var hydrate = false;
var initialChildren = null;
return createFiberRoot(containerInfo, tag, hydrate, initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError);
}
function createFiberRoot(containerInfo, tag, hydrate, initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError, transitionCallbacks) {
var root = new FiberRootNode(containerInfo, tag, hydrate, identifierPrefix, onRecoverableError);
var uninitializedFiber = createHostRootFiber(tag, isStrictMode);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
return root;
}
上面代码的大致执行流程如下:
执行完成后,会创建一个FiberRootNode,保存在ReactDomRoot实例的this._internalRoot中,充当 React 的根节点,它保存着整个应用程序所需的元数据。其 current 属性指向实际的 Fiber 树结构,每次构建新的 Fiber 树时,它都会将 current 重新指向新的 HostRoot。生成如下FiberTree结构:
当前的root属性如下:
上图中的root为当前ReactDOMRoot的实例,内部_internalRoot为当前实例的FiberRootNode,其current指向当前页面的FiberNode节点。其中tag的值为3,表示FiberNode的节点类型为HostRoot。
render
在render过程中主要执行了下面内容:
ReactDOMRoot.prototype.render = function (children) {
var root = this._internalRoot;
updateContainer(children, root, null, null);
};
function updateContainer(element, container, parentComponent, callback) {
// some other code...
var update = createUpdate(eventTime, lane);
update.payload = {
element: element
};
callback = callback === undefined ? null : callback;
var root = enqueueUpdate(current$1, update, lane);
if (root !== null) {
// 进入调度器 schedule
scheduleUpdateOnFiber(root, current$1, lane, eventTime);
}
return lane;
}
function scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
if ((executionContext & RenderContext) !== NoLanes && root === workInProgressRoot) {
// some code...
} else {
// 确保FiberRootNode被调度
ensureRootIsScheduled(root, eventTime);
}
}
function ensureRootIsScheduled(root, currentTime) {
// some code...
// 获取更新优先级,拿取最高级别的优先级任务进行执行
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
function performSyncWorkOnRoot(root) {
var exitStatus = renderRootSync(root, lanes);
var finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root, workInProgressRootRecoverableErrors, workInProgressTransitions);
return null;
}
上面的代码可以概括为下面的流程图:
其中scheduleUpdateOnFiber、ensureRootIsScheduled以及scheduleSyncCallback都是调度相关的函数,本章的重点是渲染,先暂时跳过这些内容,后期调度相关会详细讲解。渲染相关的核心函数为performSyncWorkOnRoot,以及performConcurrentWorkOnRoot函数,performSyncWorkOnRoot为同步模式,performConcurrentWorkOnRoot为并发模式
同步模式
performSyncWorkOnRoot
↓
renderRootSync // 同步渲染根节点
↓
workLoopSync // 同步工作循环(不可中断)
↓
completeUnitWork // 完成单元工作
↓
commitRoot // 提交变更到DOM
并发模式
performConcurrentWorkOnRoot
↓
renderRootConcurrent // 并发渲染根节点
↓
workLoopConcurrent // 并发工作循环(可中断)
↓
shouldYield? → 是 → 暂停并返回
↓
completeUnitWork
↓
commitRoot
上面的同步模式与并发模式的主要区别为渲染根节点的过程,同步模式创建FiberTree的过程不可中断,并发模式则可以被高优先级任务中断,而commitRoot过程则是一致的,都是同步执行。
上面代码可以看到render函数中执行了renderRootSync与commitRoot两个函数,也是React中比较重要的两个部分,一个是创建FiberNode以及对应的stateNode、flags、subtreeFlags,并生成FiberTree,另一个是根据前面创建的FiberTree,获取对应节点的flags以及subtreeFlags来进行对应的DOM节点挂载操作。
📢 注意:由于commitRoot是一次执行完成的挂载过程,为了避免浏览器产生闪烁以及重绘等,React内部进行了优化,也就是在render的complete阶段,就将子树的整个DOM树构建完成了,并将其对应的flags冒泡到父节点的subtreeFlags属性中,后续在commit阶段直接比较该属性进行相应的子树挂载即可。下面会进行详细的解析:
renderRootSync
在renderRootSync函数中的核心为do...while(true)的执行workLoopSync函数
function renderRootSync(root, lanes) {
prepareFreshStack(root, lanes);
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
}
function prepareFreshStack(root, lanes) {
root.finishedWork = null;
root.finishedLanes = NoLanes;
workInProgressRoot = root;
var rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
finishQueueingConcurrentUpdates();
return rootWorkInProgress;
}
function createWorkInProgress(current, pendingProps) {
var workInProgress = current.alternate;
if (workInProgress === null) {
// We use a double buffering pooling technique because we know that we'll
// only ever need at most two versions of a tree. We pool the "other" unused
// node that we're free to reuse. This is lazily created to avoid allocating
// extra objects for things that are never updated. It also allow us to
// reclaim the extra memory if needed.
// 双缓存机制,创建 alternate
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
workInProgress.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
}
workInProgress.flags = current.flags & StaticMask;
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
var currentDependencies = current.dependencies;
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
return workInProgress;
}
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork) {
var current = unitOfWork.alternate;
setCurrentFiber(unitOfWork);
var next;
if ( (unitOfWork.mode & ProfileMode) !== NoMode) {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentFiber();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner$2.current = null;
}
上面的代码中,主要实现了两部分重要内容:
- 1.创建
workInProgress:prepareFreshStack函数将当前FiberRootNode类型的节点传入,使用其root.current属性创建workInProgress,root.current为当前容器的FiberNode节点,也就是FiberNode(HostRoot) - 2.开启工作循环:
workLoopSync以及performUnitOfWork函数实现了对FiberTree的创建,其中beginWork是创建FiberNode,completeUnitOfWork为创建stateNode并构建以当前节点为根结点的DOM树的过程。
执行完上面的内容后,当前内存中的数据结构如下:
workLoopSync
下面是beginWork函数的主要内容,主要就是根据fiberNode的tag值执行不同的方法
function beginWork(current, workInProgress, renderLanes) {
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case IndeterminateComponent:
{
return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
}
case FunctionComponent:
{
var Component = workInProgress.type;
var unresolvedProps = workInProgress.pendingProps;
var resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderLanes);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
}
}
function updateHostRoot(current, workInProgress, renderLanes) {
var nextProps = workInProgress.pendingProps;
var prevState = workInProgress.memoizedState;
var prevChildren = prevState.element;
var nextState = workInProgress.memoizedState;
var root = workInProgress.stateNode;
var nextChildren = nextState.element;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
让我们从头进入该函数,了解一下执行过程,现在使用了如下的结构进行渲染:
<App>
<A>
<div>
<div>A</div>
<div>B</div>
</div>
</A>
</App>
beginWork阶段
第一次 workLoop
函数执行过程如下图:
这是第一次workLoop循环,当前的workInProgress === FiberNode(HostRoot),进入beginWork函数,根据当前tag === 3,进入updateHostRoot函数,然后进入reconcileChildren函数中,此时,会将 root.render(<App />)代码中的<App />函数组件,经过babel编译后的ReactElement对象作为nextChildren传入reconcileChildFibers作为函数,该函数为执行ChildReconciler(true)后返回的函数,也就是ChildReconciler函数中的闭包reconcileChildFibers函数,其中shouldTrackSideEffects参数为true。后面继续进入reconcileSingleElement函数,该函数的主要作用是创建FiberNode(<App />),并将其return属性,指向当前FiberNode(HostRoot)。后续将当前新创建的FiberNode(<App />)作为参数传入placeSingleChild函数,添加flags,其shouldTrackSideEffects === true,则FiberNode(<App />).flags === Placement,执行到当前的内存数据接口如下:
然后从placeSingleChild依次从调用栈中进行返回,其中比较重要的就是在reconcileChildren函数中,将workInProgress.child属性指向了新创建的FiberNode(<App />)节点。此时结构如下:
然后在performUnitOfWork函数中,将workInProgress指向了FiberNode(<App />)节点。此时结构如下:
第二次workLoop
此时进入下一次workLoopSync循环,流程如下:
当前循环与上一次循环不同之处在于,此时的FiberNode(<App />).tag === IndeterminateComponent并且此时workInProgress的current为null,于是进入mountIndeterminateComponent函数,将该FiberNode<App />.tag修改为FunctionComponent,然后继续执行renderWithHooks函数,在该函数内,如果是函数组价,则获取其type,也就是函数来进行执行,执行完成后的返回值,传入reconcileChildren函数内,由于current === null,所以执行mountChildFibers函数,创建FiberNode(<A />),当前数据结构如下:
在函数返回后逐渐返回调用栈中的函数,并在reconcileChildren函数中将workInProgress.child指向当前新的结点FiberNode(<A />),并在performUnitOfWork函数中将当前新创建的结点指向workInProgress,当前数据结构如下:
第三次workLoop
继续执行第三次workLoop循环,流程如下:
当前循环与上次循环过程基本一致,除了生成的
FiberNode的tag === HostComponent类型,这是在createFiberFromTypeAndProps函数中根据当前的type值决定的,当前type === div,表示为宿主组件,则将tag = HostComponent,当前数据结构如下:
依次返回调用栈中的函数,本轮循环执行完成后数据结构如下:
第四次workLoop
第四次循环流程如上图👆🏻,当前workInProgress === FiberNode(div),则在beginWork函数中根据tag === HostComponent,会进入updateHostComponent函数,babel在编译时将HostComponent组件的子元素作为children属性放在了其element.props中,然后再创建FiberNode时,保存在了FiberNode(div).pendingProps属性中。如下代码:
function createFiberFromElement(element, mode, lanes) {
var type = element.type;
var key = element.key;
var pendingProps = element.props;
var fiber = createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, lanes);
return fiber;
}
然后再updateHostComponent中执行下面代码,将其子元素传入了reconcileChildren函数
var nextProps = workInProgress.pendingProps;
var prevProps = current !== null ? current.memoizedProps : null;
var nextChildren = nextProps.children;
执行到reconcileChildFibers函数内部,发现其isArray(newChild) === true,则执行了reconcileChildrenArray函数
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) {
var resultingFirstChild = null;
var previousNewFiber = null;
var oldFiber = currentFirstChild;
var lastPlacedIndex = 0;
var newIdx = 0;
var nextOldFiber = null;
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
var _newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (_newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = _newFiber;
} else {
previousNewFiber.sibling = _newFiber;
}
previousNewFiber = _newFiber;
}
return resultingFirstChild;
}
return resultingFirstChild;
}
当前函数执行完成后的内存模型如下:
如上图所示,生成了对应的
FiberNode(div-A)节点,以及FiberNode(div-B)节点,并将其通过sibling属性进行连接。并且它们的return节点都指向了FiberNode(div)节点。
后续依次返回调用栈中的函数,本轮循环执行完成后数据结构如下:
第五次workLoop
进入第五次workLoop循环中的beginWork函数,当前workInProgress === FiberNode(div-A),流程如下:
大致流程与上次循环一致,不同点为没有其子元素,函数返回的为
null,其没有子节点,于是进入performUnitOfWork函数中的completeUnitOfWork函数。当前内存中数据结构如下:
整体的beginWork流程如下图: