網站首頁 編程語言 正文
為什么要使用fiber,要解決什么問題?
在 react16
引入 Fiber
架構之前,react 會采用遞歸方法對比兩顆虛擬DOM樹,找出需要改動的節點,然后同步更新它們,這個過程 react
稱為reconcilation(協調)。在reconcilation
期間,react
會同步執行操作,提交到真實 DOM 的更改,會一直占著瀏覽器的資源,不能中斷,中斷后就不能恢復,使得我們一些用戶操作定時器等等事件無法得到響應,是一個非常糟糕的用戶體驗。
所以我們要解決的問題就是:解決React主線程長時間占用的一個問題。 這個時候,就引入了Fiber
架構。
fiber是什么?
Fiber
可以理解為是一個執行單元,也可以理解為是一種數據結構。每一個React元素都對應一個fiber對象,我們先看看fiber
中的屬性:
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { // 作為靜態數據結構的屬性 this.tag = tag; // Fiber對應組件的類型 Function/Class/Host... this.key = key; // key屬性 this.elementType = null; // 大部分情況同type,某些情況不同,比如FunctionComponent使用React.memo包裹 this.type = null; // 對于 FunctionComponent,指函數本身,對于ClassComponent,指class,對于HostComponent,指DOM節點tagName this.stateNode = null; // Fiber對應的真實DOM節點 // 用于連接其他Fiber節點形成Fiber樹 this.parent = null; // 指向父級Fiber節點 this.child = null; // 指向子Fiber節點 this.sibling = null; // 指向右邊第一個兄弟Fiber節點 this.index = 0; this.ref = null; // 作為動態的工作單元的屬性 —— 保存本次更新造成的狀態改變相關信息 this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; // class 組件 Fiber 節點上的多個 Update 會組成鏈表并被包含在 fiber.updateQueue 中。 函數組件則是存儲 useEffect 的 effect 的環狀鏈表。 this.memoizedState = null; // hook 組成單向鏈表掛載的位置 this.dependencies = null; this.mode = mode; // Effects this.flags = NoFlags; this.subtreeFlags = NoFlags; this.deletions = null; // 調度優先級相關 this.lanes = NoLanes; this.childLanes = NoLanes; // 指向該fiber在另一次更新時對應的fiber this.alternate = null; }
數據結構
React Fiber 就是采用鏈表實現的,主要就是通過以下這幾個屬性表示:
this.parent = null; // 指向父級Fiber節點 this.child = null; // 指向子Fiber節點 this.sibling = null; // 指向右邊第一個兄弟Fiber節點
假如我們要渲染下面這個元素樹:
<div> <h1> <p> <a></a> </p> </h1> <h2></h2> </div>
我們看一下它的Fiber結構樹:
每個fiber
元素都有這三個屬性,觀察上面圖發現:
-
parent
:指向父級Fiber
節點: -
child
:指向子Fiber
節點 -
sibling
:指向右邊的兄弟節點
執行單元
我們可以把每個fiber
當做一個執行單元,每次執行完一個執行單元。React
會去檢測還剩多少時間,如果沒有時間就將控制權讓給瀏覽器,如果還有時間就去執行下一個執行單元。
這里就涉及到了一個問題,react
如何和瀏覽器進行控制權的交接,瀏覽器何時空閑呢?。我們先來了解一下瀏覽器的工作:
瀏覽器工作:
在瀏覽器中,我們所看到的頁面是一幀一幀畫出來的,渲染的幀率與設備的刷新率保持一致。通常情況下,我們的設備都是60Hz
,也就是說,1s
屏幕會刷新60
次。當每秒內繪制的幀數(FPS
)超過60
時,頁面渲染是流暢的,當幀數小于60
時,會明顯感受到卡頓。下面來看完整的一幀中,瀏覽器具體做了哪些事情:
- 首先需要處理輸入事件,能夠讓用戶得到最早的反饋
- 接下來是處理定時器,需要檢查定時器是否到時間,并執行對應的回調
- 接下來處理
Begin Frame
(開始幀),即每一幀的事件,包括window.resize
、scroll
、media query change
等 - 接下來執行請求動畫幀
requestAnimationFrame(rAF
),即在每次繪制之前,會執行rAF
回調 - 緊接著進行
Layout
操作,包括計算布局和更新布局,即這個元素的樣式是怎樣的,它應該在頁面如何展示 - 接著進行
Paint
操作,得到樹中每個節點的尺寸與位置等信息,瀏覽器針對每個元素進行內容填充 - 到這時以上的六個階段都已經完成了,接下來處于空閑階段(
Idle Peroid
),可以在這時執行requestIdleCallback
里注冊的任務
這樣我們把工作單元的任務放到requestIdleCallback
回調當中,如果瀏覽器處理完上述的任務(布局和繪制之后),還有盈余時間,這個時候就可以執行我們的工作單元了。每次執行完一個執行單元。React
會去檢測還剩多少時間,如果沒有時間就將控制權讓給瀏覽器。直至,React
和瀏覽器通過合作式調度完美配合,實現高性能應用。
Fiber執行原理
從根節點開始調度和渲染可以分為兩個階段:render
和commit
。 先來了解下這幾個關鍵名詞:
workInProgress tree:
workInProgress
代表當前正在執行更新的 Fiber
樹。在 setState
或者渲染 后,會構建一顆 Fiber
樹,也就是 workInProgress tree
,
currentFiber tree:
首次渲染之后,React
會生成一個對應于 UI
渲染的 fiber
樹,稱之為 current 樹。在新一輪更新時 workInProgress tree
再重新構建,新workInProgress
的節點通過 alternate
屬性和 currentFiber
的節點建立聯系。
Effects list:
effect list
可以理解為是一個存儲 effect
副作用列表容器。
render階段:
在render
階段中,會找到所有節點的變更,比如說節點新增,編輯,刪除等等。這些變更React
稱之為副作用effect。在這個階段中,也可以認為是diff
階段,主要就是對比currentFiber tree
和workInProgress tree
之間的差異,然后打上標記
。
在這個階段,任務是可以終止的。React 可以根據當前可用的時間片處理一個或多個 fiber
節點,并且得益于 fiber
對象中存儲的元素上下文信息以及構成的鏈表結構,使其能夠將執行到一半的工作仍保存在內存的鏈表中。在重新獲得控制權后,又可以根據保存在內存中的上下文信息快速找到停止的fiber
節點,然后繼續工作執行工作單元。
遍歷節點過程:
遍歷Fiber tree
時采用的是后序遍歷方法
- 從頂部開始遍歷
- 如果有child節點,且還未遍歷,遍歷child節點
- 如果有child節點,且已經遍歷過,則遍歷sibling節點。
- 如果沒有child節點,返回父節點
- 如果最后返回的節點為頂部,表示所有節點遍歷完成。
收集effect list:
在遍歷的過程中,我們會去收集所有變更的節點產出的effect
,每個effect
通過鏈表的方式鏈接。每個 fiber 有兩個屬性
- firstEffect:指向第一個有副作用的子fiber
- lastEffect:指向最后一個有副作用的子fiber
中間的使用nextEffect
做成一個單鏈表。
commit階段:
與render
階段不同,commit
階段是同步操作的。
為什么commit必須是同步的操作的?
因為在commit
階段是更新真實的dom
,所以更新dom不可能一點一點去更新,這樣用戶體驗會極差。所以commit
階段必須是同步執行,一次更新到位。
首先的事情是遍歷effect-list
列表,拿到每一個 effect
存儲的信息,根據副作用類型 effectTag
執行相應的處理并提交更新到真正的 DOM
。所有的effects
都會在layout phase
階段之前被處理。當該階段執行結束時,workInProgress樹
會被替換成current樹
。到這里,根據收集到的變更信息完成了刷新操作。
原文鏈接:https://juejin.cn/post/7161704351847677983
相關推薦
- 2022-10-28 go語言?nil使用避坑指南_Golang
- 2022-10-29 C++通過Makefile定義宏參數,傳入程序
- 2022-11-18 Python學習之字符串常用操作詳解_python
- 2022-03-16 C#?使用Fluent?API?創建自己的DSL(推薦)_C#教程
- 2022-08-04 深入理解pytorch庫的dockerfile_python
- 2022-03-17 Android跳轉三方應用實例代碼_Android
- 2022-04-17 docker 執行python腳本 并查看日志
- 2022-07-29 如何通過redis減庫存的秒殺場景實現_Redis
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支