commitMutationEffects
commitMutationEffects是一个while循环,遍历包含effect tag的fiber节点的链表。
遍历的每一个Fiber节点:
- 会判断是否包含ContentReset,即是否需要重置文本节点。
- 是否包含Ref
- 判断是否包含Placement(插入dom)、Update(更新)、Delection(删除)、Hydrating(ssr)相关
- Placement,执行commitPlacement(nextEffect)
- 当前环境不支持mutation,则直接返回。
placement
commitPlacement
function commitPlacement(finishedWork) {
var parentFiber = getHostParentFiber(finishedWork); // Note: these two variables *must* always be updated together.
switch (parentFiber.tag) {
case HostComponent:
{
var parent = parentFiber.stateNode;
if (parentFiber.flags & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent); // Clear ContentReset from the effect tag
parentFiber.flags &= ~ContentReset;
}
var before = getHostSibling(finishedWork); // We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
case HostRoot:
case HostPortal:
{
var _parent = parentFiber.stateNode.containerInfo;
var _before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(finishedWork, _before, _parent);
break;
}
// eslint-disable-next-line-no-fallthrough
default:
throw new Error('Invalid host parent fiber. This error is likely caused by a bug ' + 'in React. Please file an issue.');
}
}
根据当前fiber节点,找到离他最近的Host Fiber节点,包括HostComponent、HostRoot、HostPortal(通过React.createHostPorta传进来的),这几种类型共同点:都对应有dom节点。
当找到parentFiber时,如果parentFiber包含ContentReset的Effect tag,就需要重置resetTextContent,ContentReset Effect tag并取反(即删除这个effctTag)。
接下来,找getHostSibling,当前Fiber类型的兄弟节点。
为什么要找兄弟节点?
插入一个dom节点有两种方式:一种是sibling.insertBefore方法,另一个时parentNode.append(child)。因此如果使用insertBefore就需要寻找兄弟节点,如果是append,则需要parentNode节点。
function insertOrAppendPlacementNode(node, before, parent) {
var tag = node.tag;
var isHost = tag === HostComponent || tag === HostText;
if (isHost) {
var stateNode = node.stateNode;
//如果当前节点的兄弟节点存在,就执行insertBefore,否则appendChild
if (before) {
insertBefore(parent, stateNode, before);
} else {
appendChild(parent, stateNode);
}
} else if (tag === HostPortal) ; else {
var child = node.child;
if (child !== null) {
insertOrAppendPlacementNode(child, before, parent);
var sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNode(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
//调用Dom的insertBefore方法
function insertBefore(parentInstance, child, beforeChild) {
parentInstance.insertBefore(child, beforeChild);
}
function insertInContainerBefore(container, child, beforeChild) {
if (container.nodeType === COMMENT_NODE) {
container.parentNode.insertBefore(child, beforeChild);
} else {
container.insertBefore(child, beforeChild);
}
}
//调用Dom的appendChild方法
function appendChild(parentInstance, child) {
parentInstance.appendChild(child);
}
getHostParentFiber
getHostParentFiber:一直向上递归查找最近的host类型Fiber节点,知道找到为止。
function getHostParentFiber(fiber) {
var parent = fiber.return;
while (parent !== null) {
if (isHostParent(parent)) {
return parent;
}
parent = parent.return;
}
throw new Error('Expected to find a host parent. This error is likely caused by a bug ' + 'in React. Please file an issue.');
}
getHostSibling
function getHostSibling(fiber) {
// We're going to search forward into the tree until we find a sibling host
// node. Unfortunately, if multiple insertions are done in a row we have to
// search past them. This leads to exponential search for the next sibling.
// TODO: Find a more efficient way to do this.
var node = fiber;
siblings: while (true) {
// If we didn't find anything, let's try the next sibling.
while (node.sibling === null) {
if (node.return === null || isHostParent(node.return)) {
// If we pop out of the root or hit the parent the fiber we are the
// last sibling.
return null;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
while (node.tag !== HostComponent && node.tag !== HostText && node.tag !== DehydratedFragment) {
// If it is not host node and, we might have a host node inside it.
// Try to search down until we find one.
if (node.flags & Placement) {
// If we don't have a child, try the siblings instead.
continue siblings;
} // If we don't have a child, try the siblings instead.
// We also skip portals because they are not part of this host tree.
if (node.child === null || node.tag === HostPortal) {
continue siblings;
} else {
node.child.return = node;
node = node.child;
}
} // Check if this host node is stable or about to be placed.
if (!(node.flags & Placement)) {
// Found it!
return node.stateNode;
}
}
}
有两层while循环,原因
上述Demo,从fiber树角度,一次是FiberRootNode、当前dom的Fiber节点(React.render产生的RootFiber)、RootFiber的子节点依次是App Fiber,div Fiber,Item Fiber,li Fiber。
从Dom树角度,当前的根节点是#root div,子节点依次是div、li。
可见,dom的结构与Fiber的结构并不对应。
如果在item前插入p节点,如何插入?dom数中是在li调用insertBefore插入p。所以在Fiber节点中,需要先找到li。但是p对应的兄弟节点不是li,而是Item。Item的子Fiber节点才是li,因此我们要寻找fiber节点的兄弟节点,这个查找可能是跨层级的,因此getHostSibling有两层循环。
当执行玩commitComponent以后,dom节点已经插入dom数中,需要删除Placement。
PlacementAndUpdate
要先调用插入的commitPlacement,再调update的commitWork。
commitWork
FunctionComponent
FunctionComponent、ForwardRef、MemoComponent、SimpleComponent、Block,这些节点都与FunctionComponent相关,这些case调用commitHookEffecListUnmount,会调用useLayoutEffect的销毁函数。
commitHookEffecListUnmount:会遍历effectList,如果出入的effect包含了传入的tag(hookLayout),需要先执行他们的销毁函数,即useLayoutEffect回调函数的返回值。所以在执行任何effctTag函数之前,都会执行useLayoutEffect的销毁函数。
HostComponnet
HostComponnet:会调用commitUpdate方法,接受的参数updatePayload,即updateQueue属性(Fiber节点props和children等属性改变的值,第i是key,第i+1项是value)。
commitUpdate调用UpdateProperties来更新dom的属性
Deletion
commitDeletion
支持mutation,调用unmoutHostComponents
unmoutHostComponents
要先删除一个节点,需先找到这个节点的父级节点,
根据不同的tag,找到父级Dom节点。
findParent: while (parent !== null) {
switch (parent.tag) {
case HostComponent:
{
hostParent = parent.stateNode;
hostParentIsContainer = false;
break findParent;
}
case HostRoot:
{
hostParent = parent.stateNode.containerInfo;
hostParentIsContainer = true;
break findParent;
}
case HostPortal:
{
hostParent = parent.stateNode.containerInfo;
hostParentIsContainer = true;
break findParent;
}
}
HostComponnet
找到父级节点后,执行commitNestUnmounts,嵌套的执行unMount,因为当我们要删除一个Fiber节点时,这个Fiber节点可能是一颗子树,这颗Fiber节点的所有的子、孙节点都需要递归删除。
删除操作:commitUnmout中
对于FunctionComponent节点,需要执行enqueuePendingPassiveHookEffectUnmout(注册需要执行的useEffect的回调函数)。所以当FunctionComponent包含useEffect,那么销毁时也会执行useEffect的函数。
对于ClassComponent时,会执行componentWillUnmout生命周期钩子。
对于HostComponent,会解绑他的Ref属性。
总结
mutation阶段,会遍历包含effectTag Fiber的链表,判断这些Fiber节点包含的effectTag,分别做对应的操作,包括重置文本节点、解绑Ref或更新Ref、插入Dom、跟新Dom、删除Dom和ssr等相关操作。