这是第一篇详细讲解ReactDOM操作的文章。文章内容发生在commit阶段。Fiber架构使得React需要维护两种树结构,一种是Fiber树,一种是DOM树。当一个DOM节点被删除时,Fiber树也会同步变化。但请注意删除操作的时机:在完成对DOM节点的其他更改(添加、修改)之前,必须先删除fiber节点,以免干扰其他操作。这是因为其他DOM操作需要循环fiber树。这时候如果有需要删除但没有删除的fiber节点,就会出现混乱。functioncommitMutationEffects(firstChild:Fiber,root:FiberRoot,renderPriorityLevel,){letfiber=firstChild;while(fiber!==null){//先删除constdeletions=fiber.deletions;if(deletions!==null){commitMutationEffectsDeletions(deletions,root,renderPriorityLevel);}//如果删除的fiber还有子节点,//递归调用commitMutationEffects处理commitMutationEffects(fiber.child,root,renderPriorityLevel);}}if(__DEV__){/*...*/}else{//执行其他DOM操作try{commitMutationEffectsImpl(fiber,root,renderPriorityLevel);}catch(error){captureCommitPhaseError(fiber,error);}}fiberfiber=fiber.sibling;}}fiber.deletions是渲染阶段的diff过程。如果纤程有子节点需要删除,会在此处添加。commitDeletion函数是删除节点的入口,通过调用unmountHostComponents实现删除。在了解删除操作之前,我们先来看一下场景。有下面的Fiber树,Node(Node是代号,不指具体节点)节点会被删除。纤树div#root||div||Node|↖|↖P——————>|a通过这个场景可以推断,当节点被删除,它后面的子树中的所有节点都要被删除。现在直接以这个场景为例,走一遍删除过程。这个过程其实就是unmountHostComponents函数的运行机制。删除过程中删除一个Node节点需要父DOM节点的参与:parentInstance.removeChild(child),所以先定位到父节点。过程是从Node的父节点开始在Fiber树中查找,找到的第一个原生DOM节点就是父节点。在示例中,父节点是div。之后从Node开始,遍历子树,子树也是fiber树,所以遍历是深度优先遍历,删除每个子节点。需要特别注意的一点是,在删除循环节点时,每个节点都会被delete操作处理,而且这里的每个节点都是fiber节点,而不是DOM节点。删除DOM节点的时机是遇到第一个原生DOM节点(HostComponent或HostText),在其子树中的所有fiber节点都被删除后删除。以上是对完整过程的简单描述。对于具体的流程,必须明确几个关键函数的职责和调用关系。unmountHostComponents函数用于删除纤程节点。删除的节点称为目标节点。它的职责是:找到目标节点的DOM层的父节点,判断如果目标节点是原生DOM类型节点,则执行3和4,否则先自己卸载后,再往下找原生DOM类型节点,然后执行3和4。遍历子树,执行fiber节点的卸载,删除目标节点的DOM节点。第3步的操作是通过commitNestedUnmounts完成的,其职责很简单。也很明确,就是遍历子树卸载节点。然后具体到各个节点的卸载过程,由commitUnmount完成。它的职责是卸载Ref并调用类组件的生命周期。HostPortal类型的fiber节点递归调用unmountHostComponents重复删除过程。下面我们来看一下不同类型组件的具体删除过程。区分被删除组件的类别和Node节点的类型有很多种可能。下面以最典型的三种类型(HostComponent、ClassComponent、HostPortal)为例来说明删除过程。首先执行unmountHostComponents,它会找到DOM层级的父节点,然后根据下面三种组件类型进行处理,我们一一来看。HostComponentNode是一个HostComponent,调用commitNestedUnmounts,从Node开始,遍历子树,开始unmount所有子Fiber。遍历过程是深度优先遍历。Delation-->Node(span)|↖|↖P——————>|a对节点逐个执行commitUnmount进行unmount,这个遍历过程其实和三种节点类似,依次为了节省篇幅,这里只说明一次。Node的fiber被卸载,然后down,p的fiber被卸载,p没有child,找到它的sibling,的fiber被卸载,找到一个down,a的fiber被卸载.这个时候就到了整个子树的叶子节点,开始向上返回。从a到,再回到Node,遍历卸载的过程就结束了。在子树中的所有fiber节点都被卸载后,可以安全地从父节点中删除Node的DOM节点。ClassComponentDelation-->Node(ClassComponent)||span|↖|↖P————————>|aNode是一个ClassComponent,它没有对应的DOM节点,必须先调用commitUnmount来卸载自己,然后去寻找第一个原生DOM类型的节点span,以此为起点遍历子树,确保每一个fiber节点都被卸载,然后从父节点中删除span。HostPortaldiv2(ContainerOfNode)↗divcontainerInfo|↗|↗Node(HostPortal)||span|↖|↖P——————>|aNode是HostPortal,没有对应的DOM节点,所以删除进程和ClassComponent基本相同,不同的是,删除它下面的第一个子fiber的DOM节点时,不是从被删除的HostPortal类型节点的DOM层级的父节点中删除,而是从HostPortal的containerInfo中删除,如如图div2所示,因为HostPortal会将子节点渲染到父组件以外的DOM节点。以上就是三类节点的删除过程。这里值得注意的是,unmountHostComponents函数执行遍历子树卸载各个节点时,一旦遇到HostPortal类型的子节点,就会再次调用unmountHostComponents,将其作为目标节点。卸载删除它和它的子树就相当于一个递归过程。commitUnmountHostComponent和ClassComponent的删除都调用了commitUnmount,FunctionComponent也调用了。其作用对于三个组件是不同的:一旦在FunctionComponent函数组件中调用了useEffect,卸载时就会调用useEffect的destroy函数。(调用commitHookEffectListUnmount执行useLayoutEffect的销毁函数)ClassComponent类组件需要调用componentWillUnmountHostComponent卸载reffunctioncommitUnmount(finishedRoot:FiberRoot,current:Fiber,renderPriorityLevel:ReactPriorityLevel,):{void{onCommitUnmount(current);switch(current)caseFunctionComponent:caseForwardRef:caseMemoComponent:caseSimpleMemoComponent:caseBlock:{constupdateQueue:FunctionComponentUpdateQueue|null=(current.updateQueue:any);如果(updateQueue!==null){constlastEffect=updateQueue.lastEffect;if(lastEffectconst!==irst)=lastEffect.next;leteffect=firstEffect;do{const{destroy,tag}=effect;if(destroy!==undefined){if((tag&HookPassive)!==NoHookEffect){//pusheffectenqueuePendingPassiveHookEffectUnmount到useEffect的destroy函数队列(current,effect);}else{//尝试使用try...catch调用destroysafelyCallDestroy(current,destroy);...}}effecteffect=effect.next;}while(effect!==firstEffect);}}return;}caseClassComponent:{safelyDetachRef(current);constinstance=current.stateNode;//调用componentWillUnmountif(typeofinstance.componentWillUnmount==='function'){safelyCallComponentWillUnmount(current,instance);}return;}caseHostComponent:{//卸载refsafelyDetachRef(current);return;}...}}综上所述,我们来回顾一下删除过程中的关键点:删除操作的时机删除的对象是谁?在哪里删除mutation基于Fiber节点对DOM进行其他操作之前,需要先删除该节点,以保证后续操作的fiber节点被保留所有有效的删除目标都是Fiber节点及其子树和DOMFiber节点对应的节点。整个轨迹沿着fibertree,卸载目标节点和所有子节点,先卸载目标节点(或以下)的DOM节点进行删除。对于原生DOM类型节点,直接从其父DOM节点中删除。对于HostPortal节点,它会将子节点渲染到外部DOM节点,因此会从该DOM节点中删除。通过理清以上三点,结合以上梳理过程,就可以逐步理清删除操作的来龙去脉了。