commitDeletion
1 )概述
- 在 react commit 阶段的 commitRoot 第二个while循环中
- 调用了 commitAllHostEffects,这个函数不仅仅处理了新增节点,更新节点
- 最后一个操作,就是删除节点,就需要调用
commitDeletion
,这里面做什么呢? - 遍历子树
- 因为删除的一个节点,虽然它可能是一个dom节点(在react中是fiber对象)
- 但对于react组件树来说,dom 节点(fiber对象)下面是可以存放 ClassComponent 这样的节点的
- 我要删除这个dom节点的同时,相当于也要删除了这个 ClassComponent
- 这个 ClassComponent 如果有生命周期方法,比如说 componentWillUnmount 这种方法
- 那么我们要去提醒它,要去调用这个方法, 如何去知道有没有呢?我们就需要去遍历子树中的每一个节点
- 同样的还有对于像 portal 这种方法,我们要从它的 container 里面去把它相关的 dom 节点去删除
- 这也是我们要遍历子树的一个原因,所以这个过程是无法避免的
- 遍历子树需要递归的过程
- 卸载 ref
- 因为我们这个doomm上面如果挂载了ref这个属性
- 那么我们在render这个dom节点它的 owner 上面
- 比如说 ClassComponent上面, 某个 ref 属性是指向这个dom节点
- 如果已经把这个dom节点删掉了, 这个ref如果还指向这个dom节点,肯定是不对的
- 这个时候, 要卸载这个ref
- 若有组件,需调用它的 componentWillUnmount 的生命周期方法
2 )源码
定位到 packages/react-reconciler/src/ReactFiberCommitWork.js#L1021
查看 commitDeletion
function commitDeletion(current: Fiber): void {
// dom 环境 默认 true
if (supportsMutation) {
// Recursively delete all host nodes from the parent.
// Detach refs and call componentWillUnmount() on the whole subtree.
unmountHostComponents(current);
} else {
// Detach refs and call componentWillUnmount() on the whole subtree.
commitNestedUnmounts(current);
}
detachFiber(current);
}
-
进入
unmountHostComponents
function unmountHostComponents(current): void { // We only have the top Fiber that was deleted but we need recurse down its // children to find all the terminal nodes. let node: Fiber = current; // Each iteration, currentParent is populated with node's host parent if not // currentParentIsValid. let currentParentIsValid = false; // Note: these two variables *must* always be updated together. let currentParent; let currentParentIsContainer; while (true) { if (!currentParentIsValid) { let parent = node.return; // 进入while循环 findParent: while (true) { invariant( parent !== null, 'Expected to find a host parent. This error is likely caused by ' + 'a bug in React. Please file an issue.', ); // 如果它们是这 3 个之一,它们就会跳出这个 while 循环 switch (parent.tag) { case HostComponent: currentParent = parent.stateNode; currentParentIsContainer = false; break findParent; // 注意,break的是上面对应的while循环,而非当前 switch, 下同如此 case HostRoot: currentParent = parent.stateNode.containerInfo; currentParentIsContainer = true; break findParent; case HostPortal: currentParent = parent.stateNode.containerInfo; currentParentIsContainer = true; break findParent; } // 没有符合条件的,向上去找 parent = parent.return; } // 跳出这个while循环之后,他就会设置 parentparentisvalid 为 true currentParentIsValid = true; } if (node.tag === HostComponent || node.tag === HostText) { commitNestedUnmounts(node); // After all the children have unmounted, it is now safe to remove the // node from the tree. // 上面操作的 currentParentIsContainer 变量,执行不同的 remove 方法,确定从哪里删掉 if (currentParentIsContainer) { removeChildFromContainer((currentParent: any), node.stateNode); // 从container中删除 } else { removeChild((currentParent: any), node.stateNode); // 从父节点中删除 } // Don't visit children because we already visited them. } else if (node.tag === HostPortal) { // When we go into a portal, it becomes the parent to remove from. // We will reassign it back when we pop the portal on the way up. currentParent = node.stateNode.containerInfo; currentParentIsContainer = true; // Visit children because portals might contain host components. if (node.child !== null) { node.child.return = node; node = node.child; continue; // 找到 child } } else { commitUnmount(node); // Visit children because we may find more host components below. if (node.child !== null) { node.child.return = node; node = node.child; continue; } } // 整棵树遍历完了,回到了顶点,结束 if (node === current) { return; } // 树没有兄弟节点,向上去寻找 // 进入了这个循环,说明一侧的子树找完了,开始找兄弟节点了 while (node.sibling === null) { if (node.return === null || node.return === current) { return; } node = node.return; // 向上寻找 if (node.tag === HostPortal) { // When we go out of the portal, we need to restore the parent. // Since we don't keep a stack of them, we will search for it. currentParentIsValid = false; } } // 在循环的最外面,找兄弟节点 node.sibling.return = node.return; node = node.sibling; // 找兄弟节点 } }
- 进入
commitUnmount
// User-originating errors (lifecycles and refs) should not interrupt // deletion, so don't let them throw. Host-originating errors should // interrupt deletion, so it's okay function commitUnmount(current: Fiber): void { onCommitUnmount(current); switch (current.tag) { case FunctionComponent: case ForwardRef: case MemoComponent: case SimpleMemoComponent: { const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any); if (updateQueue !== null) { const lastEffect = updateQueue.lastEffect; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { const destroy = effect.destroy; if (destroy !== null) { safelyCallDestroy(current, destroy); } effect = effect.next; } while (effect !== firstEffect); } } break; } case ClassComponent: { // 这里是卸载 ref 的操作 // 因为Ref是可以作用在classcomponent上面的 // classcomponent,具有instance而不像function component 没有 instance safelyDetachRef(current); const instance = current.stateNode; // 然后需要调用它的 componentWillUnmount 这个方法 if (typeof instance.componentWillUnmount === 'function') { safelyCallComponentWillUnmount(current, instance); } return; } case HostComponent: { safelyDetachRef(current); // 只卸载 ref return; } case HostPortal: { // TODO: this is recursive. // We are also not using this parent because // the portal will get pushed immediately. if (supportsMutation) { unmountHostComponents(current); // 注意,这里走了一个递归,也就是调用上级函数,对应上面的 commitNestedUnmounts } else if (supportsPersistence) { emptyPortalContainer(current); } return; } } }
- 进入
commitNestedUnmounts
// 要调用这个方法,说明我们遇到了一个 HostComponent 节点或 HostText 节点,主要是针对 HostComponent function commitNestedUnmounts(root: Fiber): void { // While we're inside a removed host node we don't want to call // removeChild on the inner nodes because they're removed by the top // call anyway. We also want to call componentWillUnmount on all // composites before this host node is removed from the tree. Therefore // we do an inner loop while we're still inside the host node. let node: Fiber = root; // 一进来就是一个 while true 循环,对每一个节点执行 commitUnmount // 在这个过程中如果找到了有 HostPortal,也对它执行这个方法 // 它又会去调用我们刚才的那个方法,这就是一个嵌套的递归调用的一个过程 // 最终目的是要把整个子树给它遍历完成 while (true) { commitUnmount(node); // 注意这里,一进来就执行这个,这个方法就是上面的那个方法 // Visit children because they may contain more composite or host nodes. // Skip portals because commitUnmount() currently visits them recursively. if ( node.child !== null && // If we use mutation we drill down into portals using commitUnmount above. // If we don't use mutation we drill down into portals here instead. (!supportsMutation || node.tag !== HostPortal) ) { node.child.return = node; node = node.child; continue; } if (node === root) { return; } // node 一定是 root 节点的子树, 向上找含有兄弟节点的节点 while (node.sibling === null) { if (node.return === null || node.return === root) { return; } node = node.return; } // 找它的 sibling 兄弟节点,继续执行 while 循环 node.sibling.return = node.return; node = node.sibling; } }
- 上面的代码完美阐述了删除中间的某个节点,如何处理其子节点的过程,包含 portal 的处理
- 进入
-
commitDeletion
描述了整个删除的流程 -
最重要的就是理解这个算法它如何进行递归的调用来遍历整棵子树每一个节点的过程
-
对于 Portal,ClassComponent,还有 HostComponent,会有不同的操作
-
需要注意的是,对于HostComponent的子树的遍历会放到这个
commitNestedUnmounts
方法里面去做 -
对于这个
unmountHostComponents
方法,它遍历的过程的目的是- 找到所有的 HostComponent 来调用这个
commitNestedUnmounts
方法 - 对于 Portal 和 ClassComponent,它们都会去找自己的 child 的节点
- 而只有对于 HostCommonent,它才会调用嵌套的递归的方法来遍历它的子树
- 找到所有的 HostComponent 来调用这个
-
对于这个整体流程,用下面的图来看下,比如说,要删除图中 App下的 div 节点
第一种场景
- 对这个节点调用了
commitUnmount
方法 - 然后去找它的child就是Input, 同样也调用
commitUnmount
这个方法 - 它符合
if ( node.child !== null && (!supportsMutation || node.tag !== HostPortal) )
这个条件,并 continue - 继续向下找,找到 input, 同样调用
commitUnmount
这个方法, 这时候,node.child === null, node !== root, 于是会执行while (node.sibling === null) { if (node.return === null || node.return === root) { return; } node = node.return; } // 找它的 sibling 兄弟节点,继续执行 while 循环 node.sibling.return = node.return; node = node.sibling;
- 这时候要找 input的兄弟节点,没有兄弟节点,符合
while (node.sibling === null)
- 这时候执行这个 while, 向上查找到 Input, 发现 Input是有兄弟节点的,不符合
while (node.sibling === null)
,跳出 - 这时候,node 就是 List (Input的兄弟节点),对 List 节点执行
commitUnmount
方法,继续执行 - 到
if ( node.child !== null && (!supportsMutation || node.tag !== HostPortal) )
这里 - List的child存在,并且List不是HostPortal, 这时候就向下去查找,就到了第一个 span 节点
- 这时候,span节点没有child, 就会找它的sibling, 找到button,发现没有兄弟节点了,就找它的return
- 最后一个button的return是 List, 而List又是当前循环的root, 这时候,整个方法,内外循环都停止了
- 这个过程,我们把每个节点都遍历到了,对每个节点都执行了
commitUnmount
方法
第二种场景
- 与第一种场景不同,这里的第一个span变成了 Portal,其下有一个 div
- 前一部分与第一种场景类似,当遍历到 Portal 时,调用
commitUnmount
方法,进入其内部 - 在 switch case 中匹配到了
HostPortal
,调用了unmountHostComponents
方法,并进入其内部 - 在 else if 中匹配到了
HostPortal
,存在child, 找到其child, 也就是 div 节点,继续内部循环 - 匹配到了
HostComponent
, 需要调用commitNestedUnmounts
, 这个div只有一个节点,执行完成后 - 接着调用下面的
removeChildFromContainer
方法,因为对于 Portal来说,currentParentIsContainer
是 true - 接着往下执行到 while 里面的
if (node.return === null || node.return === root)
,它的 return 是root,由此返回结束循环 - 返回到 调用的
commitUnmount
里面, 看到 case HostPortal 最后是return, 也就是这个方法结束了 - 返回到
commitNestedUnmounts
的 while true 里面的commitUnmount
下面的代码继续执行,会跳过2个if - 直接进入 while, 这时候会找 Portal 节点的sibling, 也就是 span, 接着重复场景1向上返回,最终返回到App之下的这个div