日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

React更新渲染原理深入分析_React

作者:前端開發小司機 ? 更新時間: 2023-01-29 編程語言

當我們調用 setState 之后發生了什么?react經歷了怎樣的過程將新的 state 渲染到頁面上?

一次react更新,核心就是對虛擬dom進行diff,找出最少的需要變化的dom節點,然后對其進行相應的dom操作,用戶即可在頁面上看到更新。但 react 作為廣泛使用的框架,需要考慮更多的因素,考慮多個更新的優先級,考慮主線程占用時長,考慮diff算法復雜度,考慮性能。。等等,本文就來探討一下react在其內部是如何處理數據更新的。

react在內部使用fiber這種數據結構來作為虛擬dom【react16+】,它與dom tree一一對應,形成fiber tree,一次react更新,本質是fiber tree結構的更新變化。而fiber tree結構的更新,用更專業的術語來講,其實就是fiber tree的協調(Reconcile)。Reconcile中文意思是調和、使一致,協調fiber tree,就是調整fiber tree的結構,使其和更新后的jsx模版結構、dom tree保持一致。

react從16起,將更新機制分為三個模塊,也可以說是三個步驟,分別是Schedule【調度】、Reconcile【協調】、render【渲染】

Schedule

為什么需要Schedule?

首先我們要知道react在進行協調時,提供了兩種模式:Legacy mode 同步阻塞模式和 Concurrent mode 并行模式。

不同上下文中的更新會觸發不同的模式,如果是在 eventsetTimeoutnetwork requestcallback 中觸發更新,react 會采用 Legacy 模式。如果更新與 SuspenseuseTransitionOffScreen 相關,那么 react 會采用 Concurrent 模式。

Legacy mode

Legacy mode在協調時會啟動 workLoopSyncworkLoopSync 開始工作以后,要等到所有 fiber node 都處理完畢以后,才會結束工作,也就是 fiber tree 的協調過程不可中斷。

Legacy mode存在的問題:如果 fiber tree 的結構很復雜,那么協調 fiber tree 可能會占用大量的時間,導致主線程會一直被 js 引擎占用,渲染引擎無法在規定時間(瀏覽器刷新頻率 - 16.7ms)內完成工作,使得頁面出現卡頓(掉幀),影響用戶體驗。

Concurrent mode

鑒于Legacy mode存在的問題,react團隊在react 16中提出了 Concurrent mode的概念,并在react 18中開放使用。react16、17一直為此做準備。

Concurrent 模式最大的意義在于,使用Concurrent 模式以后的react的應用可以做到:

  • 協調可以中斷、恢復;不會長時間阻塞瀏覽器渲染
  • 高優先級更新可以中斷低優先級更新,優先渲染

那么,怎么做到這兩點呢?

事實上,Schedule就是用來完成這個任務的,調度任務的優先級,使高優先級任務優先進入Reconcile,并且提供中斷和恢復機制。

時間切片

react采用時間切片的方式來實現協調的中斷和恢復,Concurrent mode在協調時會啟動 workLoopConcurrentworkLoopConcurrent 開始工作以后,每次協調 fiber node 時,都會判斷當前時間片是否到期。如果時間片到期,會停止當前 workLoopConcurrent,讓出主線程,然后請求下一個時間片繼續協調。

協調的中斷及恢復,類似于瀏覽器的eventloop,js引擎和渲染引擎互斥,在主線程中交替工作。

我們可以通過模擬 eventLoop來實現時間分片以及重新請求時間片。一段 js 程序,如果在規定時間內沒有結束,那我們可以主動結束它,然后請求一個新的時間片,在下一個時間片內繼續處理上一次沒有結束的任務。

let taskQueue = [];   // 任務列表
let shouldTimeEnd = 5ms;   // 一個時間片定義為 5ms
let channel = new MessageChannel();  // 創建一個 MessageChannel 實例
function wookLoop() {
    let beginTime = performance.now();  // 記錄開始時間
    while(true) { // 循環處理 taskQueue 中的任務
        let currentTime = performance.now();  // 記錄下一個任務開始時的時間
        if (currentTime - beginTime >= shouldTimeEnd) break;  // 時間片已經到期,結束任務處理
        processTask();  // 時間片沒有到期,繼續處理任務
  }
    if (taskQueue.length) { // 時間片到期,通過調用 postMessage,請求下一個時間片
        channel.port2.postMessage(null); 
  }
}
channel.port1.onmessage = wookLoop;  // 在下一個時間片內繼續處理任務
workLoop(); 

和瀏覽器的消息隊列 一樣, react 也會維護一個任務隊列 taskQueue,然后通過 workLoop 遍歷 taskQueue,依次處理 taskQueue 中的任務。

taskQueue 中收集任務是有先后處理順序的,workLoop 每次處理 taskQueue 中的任務時,都會挑選優先級最高的任務進行處理。

每觸發一次 react 更新,意味著一次 fiber tree 的協調,但協調并不會在更新觸發時立刻同步進行。相反,react 會為這一次更新,生成一個 task,并添加到 taskQueue 中,fiber tree 的協調方法會作為新建 taskcallback。當 wookLoop 開始處理該 task 時,才會觸發 taskcallback,開始 fiber tree 的協調。

任務的優先級

react在內部定義了 5 種類型的優先級,以及對應的超時時間timeout

  • ImmediatePriority, 直接優先級,對應用戶的 clickinputfocus 等操作; timeout為 -1,表示任務要盡快處理;
  • UserBlockingPriority,用戶阻塞優先級,對應用戶的 mousemovescroll 等操作;timeout 為 250 ms;
  • NormalPriority,普通優先級,對應網絡請求、useTransition 等操作; timeout 為 5000 ms;
  • LowPriority,低優先級(未找到應用場景);timeout 為 10000 ms;
  • IdlePriority,空閑優先級,如 OffScreen; timeout 為 1073741823 ms;

5 種優先級的順序為: ImmediatePriority > UserBlockingPriority > NormalPriority > LowPriority > IdlePriority

在確定了任務的優先級以后,react 會根據優先級為任務計算一個過期時間 expirationTime,即 expirationTime = currentTime + timeout,然后根據 expirationTime 時間來決定任務處理的先后順序。

expirationTime越小的任務會被排在task隊列的越前面,之所以需要timeout,而不是直接對比優先級等級,是為了避免低優先級任務長時間被 插隊而導致一直無響應;同時,在時間分片到期時,需要根據expirationTime判斷下一個要處理的任務是否過期,如果已過期,就不能讓出主線程,需要立即處理。

??注:react17中用Lanes重構了優先級算法,此處不展開陳述,有興趣的同學可查閱相關文檔。

獲取最先處理的task

react 采用了小頂堆來存儲task,實現最小優先隊列,即 taskQueue 是一個小頂堆,放在堆頂的task是需要最先處理的。

使用最小堆時,有三個操作:pushpoppeek

push,入堆操作,即將 task 添加到 taskQueue 中。添加一個新創建的 task 時,會將 task 添加到最小堆的堆底,然后對最小堆做自底向上的調整。調整時,會比較堆節點(task) 的 expirationTime,將 expirationTime 較小的 task 向上調整。* peek,獲取堆頂元素,即獲取需要最先處理的 task,執行 taskcallback,開始 fiber tree 的協調。* pop,堆頂元素出堆,即 task 處理完畢,從 taskQueue 中移除。移除堆頂元素以后,會對最小堆做自頂向下的調整。調整時,也是比較堆節點(task) 的 expirationTime,將 expirationTime 較大的 task 向下調整。### 高優先級的更新中斷低優先級的更新

Concurrent 模式下,如果在低優先級更新的協調過程中,有高優先級更新進來,那么高優先級更新會中斷低優先級更新的協調過程。

每次拿到新的時間片以后,workLoopConcurrent 都會判斷本次協調對應的優先級和上一次時間片到期中斷的協調的優先級是否一樣。如果一樣,說明沒有更高優先級的更新產生,可以繼續上次未完成的協調;如果不一樣,說明有更高優先級的更新進來,此時要清空之前已開始的協調過程,從根節點開始重新協調。等高優先級更新處理完成以后,再次從根節點開始處理低優先級更新。

Reconcile

前面說到,reconcile(協調)就是fiber tree 結構的更新,那么具體是怎樣更新的呢?本小節就來解答這個問題。

前置知識

從jsx到dom

Step1: 從jsx生成react element

jsx 模板通過 babel 編譯為 createElement 方法;執行組件方法,觸發 createElement 的執行,返回 react element

Step2: 從react element生成fiber tree

  • fiber tree 中存在三種類型的指針 childsiblingreturn。其中,child 指向第一個子節點,sibling 指向兄弟節點,return 指針指向父節點;* fiber tree 采用的深度優先遍歷,如果節點有子節點,先遍歷子節點;子節點遍歷結束以后,再遍歷兄弟節點;沒有子節點、兄弟節點,就返回父節點,遍歷父節點的兄弟節點;* 當節點的 return 指針返回 null 時,fiber tree 的遍歷結束;Step3: fiber tree生成之后,從fiber tree到真實dom,就是處理fiber tree上對應的副作用,包括:
  • 所有 dom 節點的新增;
  • componentDidMountuseEffectcallback 函數的觸發;
  • ref 引用的初始化;

雙緩存fiber tree

react 做更新處理時,會同時存在兩顆 fiber tree。一顆是已經存在的 old fiber tree,對應當前屏幕顯示的內容,稱為 current fiber tree;另外一顆是更新過程中構建的 new fiber tree,稱為 workInProgress fiber tree

current fiber treeworkInProgress fiber tree可以通過alternate指針互相訪問

當更新完成以后,使用 workInProgress fiber tree 替換掉 current fiber tree,作為下一次更新的 current fiber tree

協調的過程

協調過程中主要做三件事情:

1.為 workInProgress fiber tree 生成 fiber node

2.為發生變化的 fiber node標記副作用 effect

3.收集帶 effectfiber node

生成workInProgress fiber tree

workInProgress fiber tree 作為一顆新樹,生成 fiber node 的方式有三種:

  • 克隆(淺拷貝) current fiber node,意味著原來的 dom 節點可以復用,只需要更新 dom 節點的屬性,或者移動 dom 節點;
  • 新建一個 fiber node,意味著需要新增加一個 dom 節點;
  • 直接復用 current fiber node,表示對應的 dom 節點完全不用做任何處理;

復用的場景:當子組件的渲染方法(類組件的 render、函數組件方法)沒有觸發,(比如使用了React.memo),沒有返回新的 react element,子節點就可以直接復用 current fiber node

在日常開發過程中,我們可以通過合理使用 ShouldComponentUpdateReact.memo,阻止不必要的組件重新 render,通過直接復用 current fiber node,加快 workInProgress fiber tree 的協調,達到優化的目的。

相反,只要組件的渲染方法被觸發,返回新的 react element,那么就需要根據新的 react element 為子節點創建 fiber node(通過淺拷貝或新建)。

  • 如果能在 current fiber tree 中找到匹配節點,那么可以通過克隆(淺拷貝) current fiber node 的方式來創建新的節點;
  • 相反,如果無法在 current fiber tree 找到匹配節點,那么就需要重新創建一個新的節點;

我們常說的diff算法就是發生在這一環節。

diff算法比較的雙方是 workInProgress fiber tree 中用于構建 fiber nodereact elementcurrent fiber tree 中的 fiber node,比較兩者的 keytype,根據比較結果來決定如何為 workInProgress fiber tree 創建 fiber node

【 key 和 type 】:

key就是 jsx 模板中元素上的 key 屬性。如果不寫默認為undefinedjsx 模板轉化為 react element 后,元素的 key 屬性會作為 react elementkey 屬性。同樣的,react element 轉化為 fiber node 以后,react elementkey 屬性也會作為 fiber nodekey 屬性。

jsx 中不同的元素類型,有不同的type

<Component name="xxxx" />  //type = Component, 是一個函數
<div></div>    // type = "div", 是一個字符串
<React.Fragment></React.Fragment>  // type = React.Fragment, 是一個數字(react 內部定義的); 

jsx 模板轉化為 react element 以后,react elementtype 屬性會根據 jsx 元素的類型賦不同的值,可能是組件函數,也可能是 dom 標簽字符串,還可能是數字。 react element 轉化為 fiber node 以后,react elementtype 屬性也會作為 fiber nodetype 屬性。

綜上,判斷拷貝 current fiber node 的邏輯,概括來就是:

reactElement.key === currentFiberNode.key && reactElement.type === currentFiberNode.type, current fiber node //可以克隆;
reactElement.key !== currentFiberNode.key, current fiber node //不可克隆;
reactElement.key === currentFiberNode.key && reactElement.type !== currentFiberNode.type, current fiber node //不可克隆; 

diff 算法:

  • 已匹配的父節點的直接子節點進行比較,不跨父節點比較;
  • 通過比較 keytype 來判斷是否需要克隆 current fiber node。只有 keytype 都相等,才克隆 current fiber node 作為新的節點,否則就需要新建一個節點。key 值和節點類型typekey 的優先級更高。如果 key 值不相同,那么節點不可克隆。
  • 當比較 single react elementcurrent fiber node list 時,只需要遍歷 current fiber node list,比較每個 current fiber nodereact elementkey 值和 type。只有 keytype 都相等,react elementcurrent fiber node 才能匹配。如果有匹配的,直接克隆current fiber node,作為 react element 對應的 workInProgress fiber node。如果沒有匹配的 current fiber node,就需要為 react element 重新創建一個新的 fiber node 作為 workInProgress fiber node
  • 當比較react element listcurrent fiber node list 時,還需要通過列表下標 index 判斷 wokrInProgress fiber node 是否相對于克隆的 current fiber node 發生了移動。這也是diff中最復雜的地方。

為發生變化的fiber node標記effect

判斷節點是否發生變化

  • 節點只要是重新創建的而不是克隆自 current fiber node,那么節點就百分之百發生了變化,需要更新;* 節點克隆自 current fiber node,需要比較 props 是否發生了變化,如果 props 發生了變化,節點需要更新;* 節點克隆自 current fiber node,且是組件類型,還需要比較 state 是否發生了變化,如果 state 發生了變化,節點需要更新;常見的effect類型:
  • Placement,放置,只針對 dom 類型的 fiber node,表示節點需要做移動或者添加操作。
  • Update,更新,針對所有類型的 fiber node,表示 fiber node 需要做更新操作。
  • PlacementAndUpdate,放置并更新,只針對 dom 類型的 fiber node,表示節點發生了移動且 props 發生了變化。
  • Ref,表示節點存在 ref,需要初始化 / 更新 ref.current
  • Deletion,刪除,針對所有類型的 fiber node,表示 fiber node 需要移除。
  • Snapshot,快照,主要是針對類組件 fiber node。當類組件 fiber node 發生了 mount 或者 update 操作,且定義了 getSnapshotBeforeUpdate 方法,就會標記 Snapshot
  • Passive,主要針對函數組件 fiber node,表示函數組件使用了 useEffect。當函數組件節點發生 mount 或者 update 操作,且使用了 useEffect hook,就會給 fiber node 標記 Passive
  • Layout,主要針對函數組件 fiber node,表示函數組件使用了 useLayoutEffect。當函數組件節點發生 mount 或者 update 操作,且使用了 useLayoutEffect hook,就會給 fiber node 標記 Layout

react 使用二進制數來聲明 effect,如 Placement 為 2 (0000 0010),Update 為 4 (0000 0100)。一個 fiber node 可同時標記多個 effect,如函數組件 props 發生變化且使用了 useEffect hook,那么就可以使用 Placement | Update = 516(位運算符) 來標記。

收集帶effect的fiber node

如果一個 fiber node 被標記了 effect,那么 react 就會在這個 fiber node 完成協調以后,將這個 fiber node 收集到effectList中。當整顆 fiber tree 完成協調以后,所有被標記 effectfiber node 都被收集到一起。

收集fiber nodeeffectList 采用單鏈表結構存儲,firstEffect 指向第一個標記 effectfiber nodelastEffect 標記最后一個 fiber node,節點之間通過 nextEffect 指針連接。

由于 fiber tree 協調時采用的順序是深度優先,協調完成的順序是子節點、子節點兄弟節點、父節點,所以收集帶 effect 標記的 fiber node 時,順序也是子節點、子節點兄弟節點、父節點。

Render

render也稱為commit,是對協調過程中標記的effect 的處理

effect 的處理分為三個階段,這三個階段按照從前到后的順序為:

1.before mutation 階段 (dom 操作之前)

2.mutation 階段 (dom 操作)

3.layout 階段 (dom 操作之后)

不同的階段,處理的 effect 種類也不相同。在每個階段,react 都會從 effectList 鏈表的頭部 - firstEffect 開始,按序遍歷 fiber node, 直到 lastEffect

before mutation階段

before mutation 階段的主要工作是處理帶 Snapshot 標記的 fiber node。 從 firstEffect 開始遍歷 effect 列表,如果 fiber nodeSnapshot 標記,觸發 getSnapshotBeforeUpdate 方法。

mutation階段

mutation 階段的主要工作是處理帶 DeletionPlacementPlacementAndUpdateUpdate 標記的 fiber node。 在這一階段,涉及到 dom 節點的更新、新增、移動、刪除,組件節點刪除導致的 componentWillUnmountdestory 方法的觸發,以及刪除節點引發的 ref 引用的重置。

dom 節點的更新:

  • 通過原生的 API setAttributeremoveArrribute 修改 dom 節點的 attr
  • 直接修改 dom 節點的 style
  • 直接修改 dom 節點的 innerHtmltextContent

dom 節點的新增和移動:

  • 如果新增(移動)的節點是父節點的最后一個子節點,那么可以直接使用 appendChild 方法。
  • 如果不是最后一個節點,需要使用 insertBefore 方法。通過遍歷找到第一個沒有帶Placement標記的節點作為insertBefore的定位元素。

dom節點的刪除:

  • 如果節點是 dom 節點,通過 removeChild 移除;
  • 如果節點是組件節點,觸發 componentWillUnmountuseEffectdestory 方法的執行;
  • 如果標記 Deletion 的節點的子節點中有組件節點,深度優先遍歷子節點,依次觸發子節點的 componentWillUnmountuseEffectdestory 方法的執行;
  • 如果標記 Deletion 的節點及子節點關聯了 ref 引用,要將 ref 引用置空,及 ref.current = null(也是深度優先遍歷);

layout 階段

layout 階段的主要工作是處理帶 update 標記的組件節點和帶 ref 標記的所有節點。 工作內容如下:

  • 如果類組件節點是mount操作,觸發 componentDidMount;如果是 update 操作,觸發 componentDidUpdate
  • 如果函數組件節點時 mount 操作,觸發 useLayoutEffectcallback;如果是 update 操作,先觸發上一次更新生成的 destory,再觸發這一次的 callback
  • 異步調度函數組件的 useEffect
  • 如果組件節點關聯了 ref 引用,要初始化 ref.current;

原文鏈接:https://blog.csdn.net/web22050702/article/details/128398129

欄目分類
最近更新