系列文章目录(同步更新)
想要阅读更多我的技术文章?请到我的 GitHub 博客 Array-Huang/blog 来,如果对您有帮助的话请 Star&Watch 走一波哈(〃^ω^)
本系列文章均为讨论 React v17.0.0-alpha 的源码
下面来介绍 React Render 的“递”阶段 —— beginWork ,在《React 源码解析系列 - React 的 render 阶段(一):基本流程介绍》中我们可知 beginWork 的主要作用是创建本次循环(performUnitOfWork)主体(unitOfWork)的子 Fiber 节点,其流程如下:
从上图可知,beginWork 的工作路径有四条:
- mount (首屏渲染)时创建新的子 Fiber 节点,并返回该新建节点;
- update时若不满足复用条件,则与 mount 时一样创建新的子 Fiber 节点,并 diff 出相应的 effectTag 挂在子 Fiber 节点上,并返回该新建节点;
- update时若满足复用条件,则复用 current 树上对应的子 Fiber 节点(current.child),返回复用后的节点
- update时若满足复用条件,则复用 current 树上对应的子 Fiber 节点(current.child),直接返回
null
值;
归纳一下:
- 前两者是主要的工作路径;
- 第三条工作路径 —— “复用节点”实际上在第二条工作路径 —— reconcileChildFibers(update) 时也会有类似的实现,或者说是不同层次的“复用节点”;
- 而第四条工作路径 —— “直接返回
null
值”这就是属于“深度遍历”过程中,名为“剪枝”的优化策略,可以减少不必要的渲染,提高性能。
beginWork 的入参
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...省略函数体
}
beginWork 有3个参数,但目前我们只关注前两个:
- current:与本次循环主体(unitOfWork)对应的 current 树上的节点,即 workInProgress.alternate 。
- workInProgress :本次循环主体(unitOfWork),也即待处理的 Fiber 节点。
判断是 mount 还是 update
从 beginWork 的流程图中可知,第一个流程分支是判断当前为 mount(首屏渲染) 还是 update ;其判断的依据是:入参 current 是否为null
,这是因为 mount(首屏渲染) 时, FiberRootNode 的 current 指针指向null
,后续还有很多地方都需要根据这个判断来做不同的处理。
主要工作路径
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
mount(首屏渲染) 时会根据不同的 workInProgress.tag
(组件类型)来进入到不同的子节点创建逻辑,我们关注最常见的组件类型:FunctionComponent(函数组件) / ClassComponent(类组件) / HostComponent(对标 HTML 标签),最终这些逻辑都会进入 reconcileChildren 方法。
reconcileChildren
下面来看看 reconcileChildren 方法:
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 对于mount的组件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 对于update的组件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
从函数名 —— reconcileChildren 就能看出这是 Reconciler 模块的核心部分;这里我们看到会根据 mount(首屏渲染) 还是 update 来走不同的方法 —— mountChildFibers | reconcileChildFibers
,但不论走哪个逻辑,最终都会生成新的子 Fiber 节点并赋值给 workInProgress.child ,并作为下次循环(performUnitOfWork)执行时的循环主体(unitOfWork);
下面我们来看看这两个方法是什么。
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
从上面代码可以看出, mount 时执行的 reconcileChildFibers 和 update 时执行的 mountChildFibers 方式,实际上都是由 ChildReconciler 这个方法封装出来的,差别只在于传参不同。
ChildReconciler
下面来看 ChildReconciler :
// shouldTrackSideEffects 表示是否追踪副作用
function ChildReconciler(shouldTrackSideEffects) {
/* 内部函数集合 */
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
if (!shouldTrackSideEffects) { // 如不需要追踪副作用则直接返回
// Noop.
return;
}
/* 在当前节点(returnFiber)上标记删除目标节点 */
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete]; // 加入“待删除子节点”的数组中
returnFiber.flags |= ChildDeletion; // 标记当前节点需要删除子节点
} else {
deletions.push(childToDelete);
}
}
function placeSingleChild(newFiber: Fiber): Fiber {
/* 标记用新节点去替代原来的节点(如果有“原来的节点”的话) */
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement;
}
return newFiber;
}
// ...还有其它很多内部函数
/* 主流程 */
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
}
return reconcileChildFibers; // 返回主方法,其中已经通过闭包联系上一堆内部方法了
}
从上面的代码我们可以看出 ChildReconciler 实际上是通过闭包封装了一堆内部函数,其主要流程实际上就是 reconcileChildFibers 这个方法,而在 reconcileChildren 方法中的调用也正是调用的这个 reconcileChildFibers 方法;我们解读一下该方法的入参:
- returnFiber:当前 Fiber 节点,即 workInProgress
- currentFirstChild:current 树上对应的当前 Fiber 节点的第一个子 Fiber 节点,mount 时为 null
- newChild:子节点(ReactElement)
- lanes:优先级相关
然后我们回过头来看这 ChildReconciler 方法的入参 —— shouldTrackSideEffects ,这个参数的字面意思是“是否需要追踪副作用”,所谓的“副作用”,指的就是是否需要做 DOM 操作,需要的话就会在当前 Fiber 节点中打上 EffectTag ,即“追踪”副作用;而也仅有在 update 的时候,才需要“追踪副作用”,即把 current 这个 Fiber 节点与本次更新组件状态后的 ReactElement 做对比(diff),然后得出本次更新的 Fiber 节点,以及在该节点上打上 diff 的结果 —— EffectTag 。
子节点(ReactElement)
这里需要展开说明一下子节点(ReactElement)是怎么来的:
- 针对组件中的 jsx 代码,babel 会在编译阶段将其转换成一个
React.createElement()
调用的代码段。 - 如果是类组件,则执行其 render 成员方法,并得到
React.createElement()
执行的结果 —— 一个ReactElement 对象。 - 如果是函数组件,则直接执行,同样得到一个 ReactElement 对象。
- 如果是 HostComponent ,即一般的 HTML ,同样也是获得一个 ReactElement 对象。
- React.createElement 的源代码请看这里。
reconcileChildFibers
在 reconcileChildFibers 方法中,首先会判断 newChild 的类型,来进入到不同逻辑中。
主要有这些类型:
- ReactElement
- Portal
- React.Lazy包裹后的元素
- 数组
- 纯文本(包括 number 和 string)
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) { // 根据$$typeof属性来进一步区分类型
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_PORTAL_TYPE:
// 省略
case REACT_LAZY_TYPE:
// 省略
}
/* 处理子节点是一个数组的情况 */
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 省略
}
/* 处理纯文本 */
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
// 省略
}
$$typeof
从上面的代码中,我们看到除了直接用 newChild
的数据类型来判断走哪个代码分支外,还用了 newChild.$$typeof` 来判断,这个 `$$typeof
就是当前 ReactElement 的类型,它的值是一个 Symbol 值,并且是已经预先定义好的,我们可以看到在 ReactElement 的工厂函数中,已经对 $$typeof
复制为 REACT_ELEMENT_TYPE
了。
为什么需要有这 $$typeof` 属性呢?是因为需要防止 **XSS 攻击**:当应用允许存储并回显一个 JSON 对象时,恶意用户可构建一个**伪 ReactElement 对象**,形如下面的例子,如果 React 不加分辨,则会直接将该伪 ReactElement 对象渲染到 DOM 树上。因此从 React 0.14 版本后,React 会为每个真正的 ReactElement 添加 `$$typeof
属性,只有拥有该属性的 ReactElement 对象才会被 React 渲染;而由于该属性为 Symbol 类型,无法使用 JSON 来构造,因此便能堵住这一漏洞。
/* 恶意的json对象 */
var xssJsonObject = {
type: 'div',
props: {
dangerouslySetInnerHTML: {
__html: '/* 恶意脚本 */'
},
},
// ...
};
reconcileSingleElement
接着,我们以 ReactElement 类型的处理逻辑为示例继续往下走,会调用 reconcileSingleElement 方法。
尝试复用 current 树上对应的子 Fiber 节点
在该方法中,首先会有这么一个 while 循环:
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling); // 删除掉该child节点的所有sibling节点
const existing = useFiber(child, element.props.children); // 复用child节点
existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点
return existing;
}
} else {
if (child.elementType === elementType) {
deleteRemainingChildren(returnFiber, child.sibling); // 删除掉该child节点的所有sibling节点
const existing = useFiber(child, element.props); // 复用child节点
existing.ref = coerceRef(returnFiber, child, element); // 处理ref
existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点
return existing;
}
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child); // 在returnFiber标记删除该子节点
}
child = child.sibling; // 指针指向current树中的下一个节点
}
上面这段代码的作用是找出上次更新中, current 树对应 Fiber 节点中所有不可复用的子节点,并在 当前 Fiber 节点(returnFiber)中标记需要删除的 effectTag ;判断的标准大致是:
- 若某个 current Fiber 子节点的 key 属性与本次渲染中的 child.key 不一致,则标记删除
- 在 key 属性相同的前提下:若某个 current Fiber 子节点与本次渲染中的 child 均为 Fragment ,或是它们的 elementType 属性一致,那么则执行复用。
复用的流程基本如下:
deleteRemainingChildren(returnFiber, child.sibling)
,这是因为走到 reconcileSingleElement 这个方法中意味着当前处理节点只有一个子节点,因此找到可复用的子节点后,可以标记删除掉剩下的(sibling)子节点。const existing = useFiber(child, element.props);
,调用 useFiber 方法来复用子 Fiber 节点。existing.return = returnFiber;
,建立子 Fiber 节点(existing)与当前 Fiber 节点(returnFiber)的父子关系(return
属性)。
无法复用,创建新的 Fiber 子节点
如果没有可复用的子节点的话,会进入创建新的子节点的逻辑:
if (element.type === REACT_FRAGMENT_TYPE) {
// ...创建Fragment类型的子节点,忽略
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes); // 根据当前子节点的ReactElement来创建新的Fiber节点
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
下详细介绍如何复用子节点以及如何创建一个全新的子节点。
复用子节点 —— useFiber
复用子节点所调用的是 useFiber 方法,我们回顾下是怎么调用这个方法的:const existing = useFiber(child, element.props);
。
这里的 child
指的是确定可以复用的 current 树子 Fiber 节点,而 element.props 则是本次更新时 ReactElement 获得的 props 值(该值也被称为 pendingProps
)。
然后我们再看 useFiber 这个方法本身:
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
const clone = createWorkInProgress(fiber, pendingProps);
clone.index = 0; // 重置一下:当前子节点必然为第一个子节点
clone.sibling = null; // 重置一下:当前子节点没有sibling
return clone;
}
可以看出这个方法主要就是调用了 createWorkInProgress 方法。
createWorkInProgress
我们接下来看看 createWorkInProgress 方法干了什么:
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
/*
如果current.alternate为空(这里先不要理解成是workInProgress),
则复用current节点,再根据本次更新的props来new一个FiberNode对象
*/
if (workInProgress === null) {
// createFiber是Fiber节点(FiberNode)的工厂方法
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode, // mode属性表示渲染模式,一个二进制值
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode; // DOM 节点
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// 如果current.alternate不为空,则重置workInProgress的pendingProps/type/effectTag等属性
}
// 复制current的子节点、上次更新时的props和state
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
// 复制current的指针
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
return workInProgress;
}
这里需要关注的重点是:
- 如果 current.alternate 不为空,那此时 current.alternate 应该是上上次更新时的树节点,我们可以留意到这种场景下,并没有创建新的 Fiber 节点,而是直接复用了这个 current.alternate 节点(只是对它的一些属性进行重置),这就可以看出“双缓存”的本质,并非是“每创建一棵新的 Fiber 树就把上上次更新时的 Fiber 树抛弃掉”,而是”在创建本次更新的 Fiber 树时,尽量复用上上次更新时的 Fiber 树,保证任一时刻最多只有两棵 Fiber 树”;而所谓的 current 和 workInProgress ,其实都是相对的,只是取决于此时的 FiberRootNode 的 current 属性指向哪棵 Fiber 树而已。
- FiberNode 上的 node 属性表示渲染模式,是一个二进制值,具体定义在这里。
创建全新子节点 —— createFiberFromElement
创建全新子节点所调用的方法是 createFiberFromElement :
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let owner = null;
const type = element.type;
const key = element.key;
const pendingProps = element.props;
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
owner,
mode,
lanes,
);
return fiber;
}
可以看出,createFiberFromElement 方法主要就是执行了 createFiberFromTypeAndProps 这个方法,而该方法主要是解析确定下新节点的 tag、type 属性,并调用 createFiber 方法 new 了一个新节点对象。
reconcileChildrenArray
当一个节点有多个子节点(如:
回顾下在 reconcileChildFibers 方法中是如何调用该方法的:
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber, // 当前的Fiber节点
currentFirstChild, // current树中对应的子Fiber节点
newChild, // 本次更新的子ReactElement
lanes, // 优先级相关
);
}
与 reconcileSingleElement 方法类似,reconcileChildrenArray 实际上也是尝试复用 current 树上的对应子节点,如遇到无法复用的子节点,则创建新节点;但不同点在于, reconcileChildrenArray 需要处理的子节点实际上是一个数组,因此需要进行新数组(本次更新中创建的 ReactElement )与原数组(current 树上对应的子 Fiber 节点)间的对比,其大概思路如下:
- 根据 index 遍历新老数组元素,一一对比新老数组,对比的依据是 key 属性是否相同;
- 若 key 属性相同,则复用节点并继续进行遍历,直到遇到不能复用的情况(或老数组中的所有节点都已经被复用)则结束遍历。
- 如果老数组所有节点都已经被复用,但新数组尚有未处理的部分,则依据新数组该未处理部分来创建新的 Fiber 节点。
- 如果老数组有节点尚未被遍历(即在第一次遍历中碰到不能复用的情况而中途退出),那么将这部分放进一个 map 里,然后继续遍历新数组,看看有没有能从 map 里找到能复用的;若能复用的,则进行复用,否则创建新 Fiber 节点;对于未被复用的旧节点,则全部标记删除(deleteChild)。
需要注意的是,虽然 reconcileChildrenArray 把整个数组(newChild)的 Fiber 节点都创建出来了,但其最终 return 的实际上是数组中的第一个 Fiber 节点,换句话说:在下次 performUnitOfWork 中的循环主体(unitOfWork)实际上是这个数组中的第一个 Fiber 节点;而当这“第一个 Fiber 节点”执行到 completeWork 阶段时,会取出它的 sibling —— 也就是这个数组中的第二个 Fiber 节点来作为下次 performUnitOfWork 中的循环主体(unitOfWork)。
优化的工作路径
上文花了非常多的篇幅来一路深入介绍 beginWork 的主要工作路径,下面我们还是回到 beginWork 处:
if (current !== null) {
const oldProps = current.memoizedProps
const newProps = workInProgress.pendingProps
if (
oldProps !== newProps ||
hasLegacyContextChanged() // 判断context是否有变化
) {
/* 该didReceiveUpdate变量代表本次更新中本Fiber节点是否有变化 */
didReceiveUpdate = true
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false
switch (
workInProgress.tag
) {
// 省略
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes)
} else {
didReceiveUpdate = false
}
} else {
didReceiveUpdate = false
}
当前代码段的作用是判断当前 Fiber 节点是否有变化,其判断的依据是: props 和 fiber.type(如函数组件的函数、类组件的类、html 标签等)和 context 没有变化;并且在 Fiber 节点没有变化的前提下(!includesSomeLane(renderLanes, updateLanes)
涉及优先级暂不讨论),尝试原封不动地复用子 Fiber 节点或是直接“剪枝”:bailoutOnAlreadyFinishedWork 方法。
bailoutOnAlreadyFinishedWork
接下来我们来看 bailoutOnAlreadyFinishedWork 方法:
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// 省略
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { // 判断子节点中是否需要检查更新
return null; // 剪枝:不需要关注子节点(ReactElement)了
} else {
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
}
- 当
!includesSomeLane(renderLanes, workInProgress.childLanes) === true
时,会直接return null
,这就是上文说到的“剪枝”策略:不再关注其下的子节点,转到本节点的 completeWork 阶段。 - 不满足上述条件时,则克隆 current 树上对应的子 Fiber 节点并返回,作为下次 performUnitOfWork 的主体。
克隆 current 树上对应的子 Fiber 节点 —— cloneChildFibers
这里的“克隆 current 树上对应的子 Fiber 节点”可能会造成一些迷惑,我们直接看 cloneChildFibers 代码:
export function cloneChildFibers(
current: Fiber | null,
workInProgress: Fiber,
): void {
// 省略
/* 判断子节点为空,则直接返回 */
if (workInProgress.child === null) {
return;
}
let currentChild = workInProgress.child; // 这里怎么会是拿workInProgress.child来充当currentChild呢?解释看下文
let newChild = createWorkInProgress(currentChild, currentChild.pendingProps); // 复用currentChild
workInProgress.child = newChild;
newChild.return = workInProgress; // 让子Fiber节点与当前Fiber节点建立联系
/* 遍历子节点的所有兄弟节点并进行节点复用 */
while (currentChild.sibling !== null) {
currentChild = currentChild.sibling;
newChild = newChild.sibling = createWorkInProgress(
currentChild,
currentChild.pendingProps,
);
newChild.return = workInProgress;
}
newChild.sibling = null;
}
这里我们看到明明是拿 workInProgress.child 去创建子节点的,怎么会说成是克隆 current 树上对应的子 Fiber 节点呢?而且按理说此时还没创建子 Fiber 节点, workInProgress.child 怎么会有值呢?
其实是这样的,当前节点是在父节点的 beginWork 阶段通过 createWorkInProgress 方法创建出来的,会执行 workInProgress.child = current.child
,因此在本节点创建自己的子节点并覆盖 workInProgress.child
之前,workInProgress.child
其实指向的就是 current.child
。
EffectTag
上文说到,在 update 的场景下,除了与 mount 时一样创建子 Fiber 节点外,还会与上次渲染的子节点进行 diff ,从而得出需要进行什么样的 DOM 操作,并将其“标记”在新建的子 Fiber 节点上,下面就来介绍一下这个“标记” —— EffectTag 。
EffectTag 是 Fiber Reconciler 相对于 Stack Reconciler 的一大革新,以往 Stack Reconciler 是每 diff 出一个节点就进行 commit 的(当然,由于 Stack Reconciler 是同步执行的,因此直到所有节点都 commit 完了才会轮到浏览器 GUI 线程进行渲染,这样就不会造成“仅部分更新”的问题),而 Fiber Recconciler 则在 diff 出来后,仅在目标节点打上 effectTag ,而不会走到 commit 阶段,待所有节点都完成 render 阶段后才统一进 commit 阶段,这样便实现了 reconciler(render 阶段) 和 renderer(commit 阶段) 的解耦。
EffectTag 类型的定义
effectTag 实际上就是需要对节点需要执行的 DOM 操作(也可认为是副作用,即 sideEffect ),定义有以下这些类型(仅节选部分 EffectTag 类型):
// DOM需要插入到页面中
export const Placement = /* */ 0b00000000000010;
// DOM需要更新
export const Update = /* */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /* */ 0b00000000000110;
// DOM需要删除
export const Deletion = /* */ 0b00000000001000;
为什么需要使用二进制来表示 effectTag 呢?
这是因为同一个 Fiber 节点,可能需要执行多种类型的 DOM 操作,即需要打上多种类型的 effectTag,那么这时候只要将这些 effectTag 做“按位或”(|)运算,那么就可以汇总成当前 Fiber 节点拥有的所有 effectTag 类型了。
若要判断某个 Fiber 节点是否有某种类型的 effectTag ,其实也很简单,拿 fiber.effectTag 跟这个类型的 effectTag 所对应的二进制值来做“按位与”(&)运算,再根据运算结果是否为 NoEffect(0)
即可。
renderer 根据 EffectTag 来执行 DOM 操作
以 renderer “判断当前节点是否需要进行插入 DOM 操作”为例:
- fiber.stateNode 存在,即Fiber节点中保存了对应的 DOM 节点
- (fiber.effectTag & Placement) !== 0,即Fiber节点存在 Placement effectTag。
以上对于 update 操作都很好理解,但 mount 时在 reconcileChildren 中调用的 mountChildFibers 的要怎么办呢?
mount 时的 fiber.stateNode 为 null ,那不就不会执行插入 DOM 操作?
fiber.stateNode
会在节点的“归”阶段,即 completeWork 中进行创建。
mount 时每个节点上都会有 Placement EffectTag
?
假设 mountChildFibers 也会赋值 effectTag ,那么可以预见 mount 时整棵 Fiber 树所有节点都会有 Placement effectTag
。那么 commit 阶段在执行 DOM 操作时每个节点都会执行一次插入操作,这样大量的DOM操作是极低效的。
为了解决这个问题,在 mount 时只有 rootFiber 会赋值 Placement effectTag
,在 commit 阶段只会执行一次插入操作。