自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

React和DOM的那些事-節(jié)點(diǎn)刪除算法

開(kāi)發(fā) 前端 算法
本篇是詳細(xì)解讀React DOM操作的第一篇文章,文章所講的內(nèi)容發(fā)生在commit階段。

[[378076]]

 本篇是詳細(xì)解讀React DOM操作的第一篇文章,文章所講的內(nèi)容發(fā)生在commit階段。

Fiber架構(gòu)使得React需要維護(hù)兩類樹(shù)結(jié)構(gòu),一類是Fiber樹(shù),另一類是DOM樹(shù)。當(dāng)刪除DOM節(jié)點(diǎn)時(shí),F(xiàn)iber樹(shù)也要同步變化。但請(qǐng)注意刪除操作執(zhí)行的時(shí)機(jī):在完成DOM節(jié)點(diǎn)的其他變化(增、改)前,要先刪除fiber節(jié)點(diǎn),避免其他操作被干擾。 這是因?yàn)檫M(jìn)行其他DOM操作時(shí)需要循環(huán)fiber樹(shù),此時(shí)如果有需要?jiǎng)h除的fiber節(jié)點(diǎn)卻還沒(méi)刪除的話,就會(huì)發(fā)生混亂。 

  1. function commitMutationEffects(  
  2.   firstChild: Fiber,  
  3.   root: FiberRoot,  
  4.   renderPriorityLevel,  
  5. ) {  
  6.   let fiber = firstChild 
  7.   while (fiber !== null) { 
  8.      // 首先進(jìn)行刪除  
  9.     const deletions = fiber.deletions;  
  10.     if (deletions !== null) {  
  11.       commitMutationEffectsDeletions(deletions, root, renderPriorityLevel);  
  12.     }  
  13.     // 如果刪除之后的fiber還有子節(jié)點(diǎn),  
  14.     // 遞歸調(diào)用commitMutationEffects來(lái)處理  
  15.     if (fiber.child !== null) {  
  16.       const primarySubtreeTag = fiber.subtreeTag & MutationSubtreeTag;  
  17.       if (primarySubtreeTag !== NoSubtreeTag) {  
  18.         commitMutationEffects(fiber.child, root, renderPriorityLevel);  
  19.       }  
  20.     }  
  21.     if (__DEV__) {/*...*/} else {  
  22.       // 執(zhí)行其他DOM操作  
  23.       try {  
  24.         commitMutationEffectsImpl(fiber, root, renderPriorityLevel);  
  25.       } catch (error) {  
  26.         captureCommitPhaseError(fiber, error);  
  27.       }  
  28.     }  
  29.     fiberfiber = fiber.sibling;  
  30.   }  

fiber.deletions是render階段的diff過(guò)程檢測(cè)到fiber的子節(jié)點(diǎn)如果有需要被刪除的,就會(huì)被加到這里來(lái)。

commitDeletion函數(shù)是刪除節(jié)點(diǎn)的入口,它通過(guò)調(diào)用unmountHostComponents實(shí)現(xiàn)刪除。搞懂刪除操作之前,先看看場(chǎng)景。

有如下的Fiber樹(shù),Node(Node是一個(gè)代號(hào),并不指的某個(gè)具體節(jié)點(diǎn))節(jié)點(diǎn)即將被刪除。   

  1. Fiber樹(shù)  
  2.    div#root  
  3.       |  
  4.     <App/>  
  5.       |  
  6.      div  
  7.       |  
  8.    <Parent/>  
  9.       |  
  10.      Node  
  11.       |     ↖  
  12.       |       ↖  
  13.       P ——————> <Child>  
  14.                   |  
  15.                   a 

通過(guò)這種場(chǎng)景可以推測(cè)出當(dāng)刪除該節(jié)點(diǎn)時(shí),它下面子樹(shù)中的所有節(jié)點(diǎn)都要被刪除?,F(xiàn)在直接以這個(gè)場(chǎng)景為例,走一下刪除過(guò)程。這個(gè)過(guò)程實(shí)際上也就是unmountHostComponents函數(shù)的運(yùn)行機(jī)制。

刪除過(guò)程

刪除Node節(jié)點(diǎn)需要父DOM節(jié)點(diǎn)的參與:

  1. parentInstance.removeChild(child) 

所以首先要定位到父級(jí)節(jié)點(diǎn)。過(guò)程是在Fiber樹(shù)中,以Node的父節(jié)點(diǎn)為起點(diǎn)往上找,找到的第一個(gè)原生DOM節(jié)點(diǎn)即為父節(jié)點(diǎn)。在例子中,父節(jié)點(diǎn)就是div。此后以Node為起點(diǎn),遍歷子樹(shù),子樹(shù)也是fiber樹(shù),因此遍歷是深度優(yōu)先遍歷,將每個(gè)子節(jié)點(diǎn)都刪除。

需要特別注意的一點(diǎn)是,對(duì)循環(huán)節(jié)點(diǎn)進(jìn)行刪除,每個(gè)節(jié)點(diǎn)都會(huì)被刪除操作去處理,這里的每個(gè)節(jié)點(diǎn)是fiber節(jié)點(diǎn)而不是DOM節(jié)點(diǎn)。DOM節(jié)點(diǎn)的刪除時(shí)機(jī)是從Node開(kāi)始遍歷進(jìn)行刪除的時(shí)候,遇到了第一個(gè)原生DOM節(jié)點(diǎn)(HostComponent或HostText)這個(gè)時(shí)刻,在刪除了它子樹(shù)的所有fiber節(jié)點(diǎn)后,才會(huì)被刪除。

以上是完整過(guò)程的簡(jiǎn)述,對(duì)于詳細(xì)過(guò)程要明確幾個(gè)關(guān)鍵函數(shù)的職責(zé)和調(diào)用關(guān)系才行。刪除fiber節(jié)點(diǎn)的是unmountHostComponents函數(shù),被刪除的節(jié)點(diǎn)稱為目標(biāo)節(jié)點(diǎn),它的職責(zé)為:

  1.  找到目標(biāo)節(jié)點(diǎn)的DOM層面的父節(jié)點(diǎn)
  2.  判斷目標(biāo)節(jié)點(diǎn)如果是原生DOM類型的節(jié)點(diǎn),那么執(zhí)行3、4,否則先卸載自己之后再往下找到原生DOM類型的節(jié)點(diǎn)之后再執(zhí)行3、4
  3.  遍歷子樹(shù)執(zhí)行fiber節(jié)點(diǎn)的卸載
  4.  刪除目標(biāo)節(jié)點(diǎn)的DOM節(jié)點(diǎn)

其中第3步的操作,是通過(guò)commitNestedUnmounts完成的,它的職責(zé)很單一也很明確,就是遍歷子樹(shù)卸載節(jié)點(diǎn)。

然后具體到每個(gè)節(jié)點(diǎn)的卸載過(guò)程,由commitUnmount完成。它的職責(zé)是

  1.  Ref的卸載
  2.  類組件生命周期的調(diào)用
  3.  HostPortal類型的fiber節(jié)點(diǎn)遞歸調(diào)用unmountHostComponents重復(fù)刪除過(guò)程

下面來(lái)看一下不同類型的組件它們的具體刪除過(guò)程是怎樣的。

區(qū)分被刪除組件的類別

Node節(jié)點(diǎn)的類型有多種可能性,我們以最典型的三種類型(HostComponent、ClassComponent、HostPortal)為例分別說(shuō)明一下刪除過(guò)程。

首先執(zhí)行unmountHostComponents,會(huì)向上找到DOM層面的父節(jié)點(diǎn),然后根據(jù)下面的三種組件類型分別處理,我們挨個(gè)來(lái)看。

HostComponent

Node 是HostComponent,調(diào)用commitNestedUnmounts,以Node為起點(diǎn),遍歷子樹(shù),開(kāi)始對(duì)所有子Fiber進(jìn)行卸載操作,遍歷的過(guò)程是深度優(yōu)先遍歷。 

  1. Delation   -->      Node(span)  
  2.                      |    ↖  
  3.                      |       ↖  
  4.                      P ——————> <Child>  
  5.                                  |  
  6.                                  a 

對(duì)節(jié)點(diǎn)逐個(gè)執(zhí)行commitUnmount進(jìn)行卸載,這個(gè)遍歷過(guò)程其實(shí)對(duì)于三種類型的節(jié)點(diǎn),都是類似的,為了節(jié)省篇幅,這里只表述一次。

Node的fiber被卸載,然后向下,p的fiber被卸載,p沒(méi)有child,找到它的sibling<Child>,<Child>的fiber被卸載,向下找到a,a的fiber被卸載。此時(shí)到了整個(gè)子樹(shù)的葉子節(jié)點(diǎn),開(kāi)始向上return。由a 到 <Child>,再回到Node,遍歷卸載的過(guò)程結(jié)束。

在子樹(shù)的所有fiber節(jié)點(diǎn)都被卸載之后,才可以安全地將Node的DOM節(jié)點(diǎn)從父節(jié)點(diǎn)中移除。

ClassComponent 

  1. Delation   -->      Node(ClassComponent)  
  2.                      |  
  3.                      |  
  4.                     span  
  5.                      |    ↖  
  6.                      |       ↖  
  7.                      P ——————> <Child>  
  8.                                  |  
  9.                                  a 

Node是ClassComponent,它沒(méi)有對(duì)應(yīng)的DOM節(jié)點(diǎn),要先調(diào)用commitUnmount卸載它自己,之后會(huì)先往下找,找到第一個(gè)原生DOM類型的節(jié)點(diǎn)span,以它為起點(diǎn)遍歷子樹(shù),確保每一個(gè)fiber節(jié)點(diǎn)都被卸載,之后再將span從父節(jié)點(diǎn)中刪除。

HostPortal         

  1.    div2(Container Of Node)  
  2.            ↗  
  3. div   containerInfo  
  4.  |    ↗  
  5.  |  ↗  
  6. Node(HostPortal)  
  7.  |  
  8.  |  
  9. span  
  10.  |    ↖  
  11.  |       ↖  
  12.  P ——————> <Child>  
  13.              |  
  14.              a 

Node是HostPortal,它沒(méi)有對(duì)應(yīng)的DOM節(jié)點(diǎn),因此刪除過(guò)程和ClassComponent基本一致,不同的是刪除它下面第一個(gè)子fiber的DOM節(jié)點(diǎn)時(shí)不是從這個(gè)被刪除的HostPortal類型節(jié)點(diǎn)的DOM層面的父節(jié)點(diǎn)中刪除,而是從HostPortal的containerInfo中移除,圖示上為div2,因?yàn)镠ostPortal會(huì)將子節(jié)點(diǎn)渲染到父組件以外的DOM節(jié)點(diǎn)。

以上是三種類型節(jié)點(diǎn)的刪除過(guò)程,這里值得注意的是,unmountHostComponents函數(shù)執(zhí)行到遍歷子樹(shù)卸載每個(gè)節(jié)點(diǎn)的時(shí)候,一旦遇到HostPortal類型的子節(jié)點(diǎn),會(huì)再次調(diào)用unmountHostComponents,以它為目標(biāo)節(jié)點(diǎn)再進(jìn)行它以及它子樹(shù)的卸載刪除操作,相當(dāng)于一個(gè)遞歸過(guò)程。

commitUnmount

HostComponent 和 ClassComponent的刪除都調(diào)用了commitUnmount,除此之外還有FunctionComponent也會(huì)調(diào)用它。它的作用對(duì)三種組件是不同的:

  •  FunctionComponent 函數(shù)組件中一旦調(diào)用了useEffect,那么它卸載的時(shí)候要去調(diào)用useEffect的銷毀函數(shù)。(useLayoutEffect的銷毀函數(shù)是調(diào)用commitHookEffectListUnmount執(zhí)行的)
  •  ClassComponent 類組件要調(diào)用componentWillUnmount
  •  HostComponent 要卸載ref 
  1. function commitUnmount(  
  2.   finishedRoot: FiberRoot,  
  3.   current: Fiber,  
  4.   renderPriorityLevel: ReactPriorityLevel,  
  5. ): void {  
  6.   onCommitUnmount(current);  
  7.   switch (current.tag) {  
  8.     case FunctionComponent:  
  9.     case ForwardRef:  
  10.     case MemoComponent:  
  11.     case SimpleMemoComponent:  
  12.     case Block: {  
  13.       const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);  
  14.       if (updateQueue !== null) {  
  15.         const lastEffect = updateQueue.lastEffect;  
  16.         if (lastEffect !== null) {  
  17.           const firstEffect = lastEffect.next;  
  18.           let effect = firstEffect 
  19.           do { 
  20.              const {destroy, tag} = effect;  
  21.             if (destroy !== undefined) {  
  22.               if ((tag & HookPassive) !== NoHookEffect) {  
  23.                 // 向useEffect的銷毀函數(shù)隊(duì)列里push effect  
  24.                 enqueuePendingPassiveHookEffectUnmount(current, effect);  
  25.               } else {  
  26.                 // 嘗試使用try...catch調(diào)用destroy  
  27.                 safelyCallDestroy(current, destroy);  
  28.                 ...  
  29.               }  
  30.             }  
  31.             effecteffect = effect.next;  
  32.           } while (effect !== firstEffect);  
  33.         }  
  34.       }  
  35.       return;  
  36.     }  
  37.     case ClassComponent: {  
  38.       safelyDetachRef(current);  
  39.       const instance = current.stateNode;  
  40.       // 調(diào)用componentWillUnmount  
  41.       if (typeof instance.componentWillUnmount === 'function') {  
  42.         safelyCallComponentWillUnmount(current, instance);  
  43.       }  
  44.       return;  
  45.     }  
  46.     case HostComponent: {  
  47.       // 卸載ref  
  48.       safelyDetachRef(current);  
  49.       return;  
  50.     }  
  51.     ...  
  52.   }  

總結(jié)

我們來(lái)復(fù)盤(pán)一下刪除過(guò)程中的重點(diǎn):

  •  刪除操作執(zhí)行的時(shí)機(jī)
  •  刪除的目標(biāo)是誰(shuí)
  •  從哪里刪除

mutation在基于Fiber節(jié)點(diǎn)對(duì)DOM做其他操作之前,需要先刪除節(jié)點(diǎn),保證留給后續(xù)操作的fiber節(jié)點(diǎn)都是有效的。刪除的目標(biāo)是Fiber節(jié)點(diǎn)及其子樹(shù)和Fiber節(jié)點(diǎn)對(duì)應(yīng)的DOM節(jié)點(diǎn),整個(gè)軌跡循著fiber樹(shù),對(duì)目標(biāo)節(jié)點(diǎn)和所有子節(jié)點(diǎn)都進(jìn)行卸載,對(duì)目標(biāo)節(jié)點(diǎn)對(duì)應(yīng)的(或之下的第一個(gè))DOM節(jié)點(diǎn)進(jìn)行刪除。對(duì)于原生DOM類型的節(jié)點(diǎn),直接從其父DOM節(jié)點(diǎn)刪除,對(duì)于HostPortal節(jié)點(diǎn),它會(huì)把子節(jié)點(diǎn)渲染到外部的DOM節(jié)點(diǎn),所以會(huì)從這個(gè)DOM節(jié)點(diǎn)中刪除。明確以上三個(gè)點(diǎn)再結(jié)合上述梳理的過(guò)程,就可以逐漸理清刪除操作的脈絡(luò)。 

 

責(zé)任編輯:龐桂玉 來(lái)源: segmentfault
相關(guān)推薦

2022-07-19 13:31:18

Buddy算法內(nèi)存管理框架

2020-10-07 22:21:13

程序員技術(shù)線程

2014-06-06 16:08:17

初志科技

2011-09-19 15:40:35

2020-07-29 08:14:59

云計(jì)算云遷移IT

2021-03-07 16:31:35

Java編譯反編譯

2017-07-19 14:26:01

前端JavaScriptDOM

2018-12-26 13:22:05

NVMeNVMe-oF數(shù)據(jù)

2021-08-16 09:59:52

ReactSvelte開(kāi)發(fā)

2023-06-16 07:48:51

DOM對(duì)象JS

2012-05-31 09:53:38

IT風(fēng)云15年

2019-01-07 12:02:02

TCP長(zhǎng)連接Java

2011-05-19 16:47:50

軟件測(cè)試

2012-05-01 08:06:49

手機(jī)

2024-02-04 17:03:30

2017-05-15 21:50:54

Linux引號(hào)

2018-03-01 15:03:11

2016-01-19 21:59:50

OpenStack

2015-08-20 09:17:36

Java線程池

2021-03-18 16:05:20

SSD存儲(chǔ)故障
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)