本篇是详细解读React DOM操作的第一篇文章,文章所讲的内容发生在commit阶段。
本篇是详细解读React DOM操作的榜首篇文章,文章所讲的内容产生在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来处理
- if(fiber.child!==null){
- constprimarySubtreeTag=fiber.subtreeTag&MutationSubtreeTag;
- if(primarySubtreeTag!==NoSubtreeTag){
- commitMutationEffects(fiber.child,root,renderPriorityLevel);
- }
- }
- if(__DEV__){/*...*/}else{
- //履行其他DOM操作
- try{
- commitMutationEffectsImpl(fiber,root,renderPriorityLevel);
- }catch(error){
- captureCommitPhaseError(fiber,error);
- }
- }
- fiberfiber=fiber.sibling;
- }
- }
fiber.deletions是render阶段的diff进程检测到fiber的子节点假如有需求被删去的,就会被加到这儿来。
commitDeletion函数是删去节点的进口,它经过调用unmountHostComponents完结删去。搞懂删去操作之前,先看看场景。
有如下的Fiber树,Node(Node是一个代号,并不指的某个详细节点)节点行将被删去。
- Fiber树
- div#root
- |
- <App/>
- |
- div
- |
- <Parent/>
- |
- Node
- |↖
- |↖
- P——————><Child>
- |
- a
经过这种场景能够推测出当删去该节点时,它下面子树中的一切节点都要被删去。现在直接以这个场景为例,走一下删去进程。这个进程实际上也便是unmountHostComponents函数的运行机制。
删去进程
删去Node节点需求父DOM节点的参加:
- parentInstance.removeChild(child)
所以首要要定位到父级节点。进程是在Fiber树中,以Node的父节点为起点往上找,找到的榜首个原生DOM节点即为父节点。在比如中,父节点便是div。尔后以Node为起点,遍历子树,子树也是fiber树,因而遍历是深度优先遍历,将每个子节点都删去。
需求特别注意的一点是,对循环节点进行删去,每个节点都会被删去操作去处理,这儿的每个节点是fiber节点而不是DOM节点。DOM节点的删去机遇是从Node开端遍历进行删去的时分,遇到了榜首个原生DOM节点(HostComponent或HostText)这个时间,在删去了它子树的一切fiber节点后,才会被删去。
以上是完好进程的简述,关于详细进程要清晰几个要害函数的责任和调用联系才行。删去fiber节点的是unmountHostComponents函数,被删去的节点称为方针节点,它的责任为:
- 找到方针节点的DOM层面的父节点
- 判别方针节点假如是原生DOM类型的节点,那么履行3、4,不然先卸载自己之后再往下找到原生DOM类型的节点之后再履行3、4
- 遍历子树履行fiber节点的卸载
- 删去方针节点的DOM节点
其间第3步的操作,是经过commitNestedUnmounts完结的,它的责任很单一也很清晰,便是遍历子树卸载节点。
然后详细到每个节点的卸载进程,由commitUnmount完结。它的责任是
- Ref的卸载
- 类组件生命周期的调用
- HostPortal类型的fiber节点递归调用unmountHostComponents重复删去进程
下面来看一下不同类型的组件它们的详细删去进程是怎样的。
区别被删去组件的类别
Node节点的类型有多种可能性,咱们以最典型的三种类型(HostComponent、ClassComponent、HostPortal)为例别离阐明一下删去进程。
首要履行unmountHostComponents,会向上找到DOM层面的父节点,然后依据下面的三种组件类型别离处理,咱们挨个来看。
HostComponent
Node 是HostComponent,调用commitNestedUnmounts,以Node为起点,遍历子树,开端对一切子Fiber进行卸载操作,遍历的进程是深度优先遍历。
- Delation-->Node(span)
- |↖
- |↖
- P——————><Child>
- |
- a
对节点逐一履行commitUnmount进行卸载,这个遍历进程其实关于三种类型的节点,都是相似的,为了节约篇幅,这儿只表述一次。
Node的fiber被卸载,然后向下,p的fiber被卸载,p没有child,找到它的sibling<Child>,<Child>的fiber被卸载,向下找到a,a的fiber被卸载。此刻到了整个子树的叶子节点,开端向上return。由a 到 <Child>,再回到Node,遍历卸载的进程完毕。
在子树的一切fiber节点都被卸载之后,才能够安全地将Node的DOM节点从父节点中移除。
ClassComponent
- Delation-->Node(ClassComponent)
- |
- |
- span
- |↖
- |↖
- P——————><Child>
- |
- a
Node是ClassComponent,它没有对应的DOM节点,要先调用commitUnmount卸载它自己,之后会先往下找,找到榜首个原生DOM类型的节点span,以它为起点遍历子树,确保每一个fiber节点都被卸载,之后再将span从父节点中删去。
HostPortal
- div2(ContainerOfNode)
- ↗
- divcontainerInfo
- |↗
- |↗
- Node(HostPortal)
- |
- |
- span
- |↖
- |↖
- P——————><Child>
- |
- a
Node是HostPortal,它没有对应的DOM节点,因而删去进程和ClassComponent根本共同,不同的是删去它下面榜首个子fiber的DOM节点时不是从这个被删去的HostPortal类型节点的DOM层面的父节点中删去,而是从HostPortal的containerInfo中移除,图示上为div2,由于HostPortal会将子节点烘托到父组件以外的DOM节点。
以上是三种类型节点的删去进程,这儿值得注意的是,unmountHostComponents函数履行到遍历子树卸载每个节点的时分,一旦遇到HostPortal类型的子节点,会再次调用unmountHostComponents,以它为方针节点再进行它以及它子树的卸载删去操作,相当于一个递归进程。
commitUnmount
HostComponent 和 ClassComponent的删去都调用了commitUnmount,除此之外还有FunctionComponent也会调用它。它的效果对三种组件是不同的:
- FunctionComponent 函数组件中一旦调用了useEffect,那么它卸载的时分要去调用useEffect的毁掉函数。(useLayoutEffect的毁掉函数是调用commitHookEffectListUnmount履行的)
- ClassComponent 类组件要调用componentWillUnmount
- HostComponent 要卸载ref
- functioncommitUnmount(
- finishedRoot:FiberRoot,
- current:Fiber,
- renderPriorityLevel:ReactPriorityLevel,
- ):void{
- onCommitUnmount(current);
- switch(current.tag){
- caseFunctionComponent:
- caseForwardRef:
- caseMemoComponent:
- caseSimpleMemoComponent:
- caseBlock:{
- constupdateQueue:FunctionComponentUpdateQueue|null=(current.updateQueue:any);
- if(updateQueue!==null){
- constlastEffect=updateQueue.lastEffect;
- if(lastEffect!==null){
- constfirstEffect=lastEffect.next;
- leteffect=firstEffect;
- do{
- const{destroy,tag}=effect;
- if(destroy!==undefined){
- if((tag&HookPassive)!==NoHookEffect){
- //向useEffect的毁掉函数行列里pusheffect
- enqueuePendingPassiveHookEffectUnmount(current,effect);
- }else{
- //测验运用try...catch调用destroy
- safelyCallDestroy(current,destroy);
- ...
- }
- }
- effecteffect=effect.next;
- }while(effect!==firstEffect);
- }
- }
- return;
- }
- caseClassComponent:{
- safelyDetachRef(current);
- constinstance=current.stateNode;
- //调用componentWillUnmount
- if(typeofinstance.componentWillUnmount==='function'){
- safelyCallComponentWillUnmount(current,instance);
- }
- return;
- }
- caseHostComponent:{
- //卸载ref
- safelyDetachRef(current);
- return;
- }
- ...
- }
- }
总结
咱们来复盘一下删去进程中的要点:
- 删去操作履行的机遇
- 删去的方针是谁
- 从哪里删去
mutation在根据Fiber节点对DOM做其他操作之前,需求先删去节点,确保留给后续操作的fiber节点都是有用的。删去的方针是Fiber节点及其子树和Fiber节点对应的DOM节点,整个轨道循着fiber树,对方针节点和一切子节点都进行卸载,对方针节点对应的(或之下的榜首个)DOM节点进行删去。关于原生DOM类型的节点,直接从其父DOM节点删去,关于HostPortal节点,它会把子节点烘托到外部的DOM节点,所以会从这个DOM节点中删去。清晰以上三个点再结合上述整理的进程,就能够逐步理清删去操作的头绪。
知优网 » React和DOM的那些事-节点删去算法(react删除dom)