摘要:在上面我們已經(jīng)知道瀏覽器是一幀一幀執(zhí)行的,在兩個執(zhí)行幀之間,主線程通常會有一小段空閑時間,可以在這個空閑期調(diào)用空閑期回調(diào),執(zhí)行一些任務(wù)。另外由于這些堆棧是可以自己控制的,所以可以加入并發(fā)或者錯誤邊界等功能。
文章首發(fā)于個人博客前言
2016 年都已經(jīng)透露出來的概念,這都 9102 年了,我才開始寫 Fiber 的文章,表示慚愧呀。不過現(xiàn)在好的是關(guān)于 Fiber 的資料已經(jīng)很豐富了,在寫文章的時候參考資料比較多,比較容易深刻的理解。
React 作為我最喜歡的框架,沒有之一,我愿意花很多時間來好好的學(xué)習(xí)他,我發(fā)現(xiàn)對于學(xué)習(xí)一門框架會有四種感受,剛開始沒使用過,可能有一種很神奇的感覺;然后接觸了,遇到了不熟悉的語法,感覺這是什么垃圾東西,這不是反人類么;然后當(dāng)你熟悉了之后,真香,設(shè)計得挺好的,這個時候它已經(jīng)改變了你編程的思維方式了;再到后來,看過他的源碼,理解他的設(shè)計之后,設(shè)計得確實好,感覺自己也能寫一個的樣子。
所以我今年(對,沒錯,就是一年)就是想完全的學(xué)透 React,所以開了一個 Deep In React 的系列,把一些新手在使用 API 的時候不知道為什么的點,以及一些為什么有些東西要這么設(shè)計寫出來,與大家共同探討 React 的奧秘。
我的思路是自上而下的介紹,先理解整體的 Fiber 架構(gòu),然后再細(xì)挖每一個點,所以這篇文章主要是談 Fiber 架構(gòu)的。
介紹在詳細(xì)介紹 Fiber 之前,先了解一下 Fiber 是什么,以及為什么 React 團(tuán)隊要話兩年時間重構(gòu)協(xié)調(diào)算法。
React 的核心思想內(nèi)存中維護(hù)一顆虛擬DOM樹,數(shù)據(jù)變化時(setState),自動更新虛擬 DOM,得到一顆新樹,然后 Diff 新老虛擬 DOM 樹,找到有變化的部分,得到一個 Change(Patch),將這個 Patch 加入隊列,最終批量更新這些 Patch 到 DOM 中。
React 16 之前的不足首先我們了解一下 React 的工作過程,當(dāng)我們通過render() 和 setState() 進(jìn)行組件渲染和更新的時候,React 主要有兩個階段:
調(diào)和階段(Reconciler):官方解釋。React 會自頂向下通過遞歸,遍歷新數(shù)據(jù)生成新的 Virtual DOM,然后通過 Diff 算法,找到需要變更的元素(Patch),放到更新隊列里面去。
渲染階段(Renderer):遍歷更新隊列,通過調(diào)用宿主環(huán)境的API,實際更新渲染對應(yīng)元素。宿主環(huán)境,比如 DOM、Native、WebGL 等。
在協(xié)調(diào)階段階段,由于是采用的遞歸的遍歷方式,這種也被成為 Stack Reconciler,主要是為了區(qū)別 Fiber Reconciler 取的一個名字。這種方式有一個特點:一旦任務(wù)開始進(jìn)行,就無法中斷,那么 js 將一直占用主線程, 一直要等到整棵 Virtual DOM 樹計算完成之后,才能把執(zhí)行權(quán)交給渲染引擎,那么這就會導(dǎo)致一些用戶交互、動畫等任務(wù)無法立即得到處理,就會有卡頓,非常的影響用戶體驗。
如何解決之前的不足之前的問題主要的問題是任務(wù)一旦執(zhí)行,就無法中斷,js 線程一直占用主線程,導(dǎo)致卡頓。
可能有些接觸前端不久的不是特別理解上面為什么 js 一直占用主線程就會卡頓,我這里還是簡單的普及一下。
瀏覽器每一幀都需要完成哪些工作?頁面是一幀一幀繪制出來的,當(dāng)每秒繪制的幀數(shù)(FPS)達(dá)到 60 時,頁面是流暢的,小于這個值時,用戶會感覺到卡頓。
1s 60 幀,所以每一幀分到的時間是 1000/60 ≈ 16 ms。所以我們書寫代碼時力求不讓一幀的工作量超過 16ms。
瀏覽器一幀內(nèi)的工作
通過上圖可看到,一幀內(nèi)需要完成如下六個步驟的任務(wù):
處理用戶的交互
JS 解析執(zhí)行
幀開始。窗口尺寸變更,頁面滾去等的處理
rAF(requestAnimationFrame)
布局
繪制
如果這六個步驟中,任意一個步驟所占用的時間過長,總時間超過 16ms 了之后,用戶也許就能看到卡頓。
而在上一小節(jié)提到的調(diào)和階段花的時間過長,也就是 js 執(zhí)行的時間過長,那么就有可能在用戶有交互的時候,本來應(yīng)該是渲染下一幀了,但是在當(dāng)前一幀里還在執(zhí)行 JS,就導(dǎo)致用戶交互不能麻煩得到反饋,從而產(chǎn)生卡頓感。
解決方案把渲染更新過程拆分成多個子任務(wù),每次只做一小部分,做完看是否還有剩余時間,如果有繼續(xù)下一個任務(wù);如果沒有,掛起當(dāng)前任務(wù),將時間控制權(quán)交給主線程,等主線程不忙的時候在繼續(xù)執(zhí)行。這種策略叫做 Cooperative Scheduling(合作式調(diào)度),操作系統(tǒng)常用任務(wù)調(diào)度策略之一。
補充知識,操作系統(tǒng)常用任務(wù)調(diào)度策略:先來先服務(wù)(FCFS)調(diào)度算法、短作業(yè)(進(jìn)程)優(yōu)先調(diào)度算法(SJ/PF)、最高優(yōu)先權(quán)優(yōu)先調(diào)度算法(FPF)、高響應(yīng)比優(yōu)先調(diào)度算法(HRN)、時間片輪轉(zhuǎn)法(RR)、多級隊列反饋法。
合作式調(diào)度主要就是用來分配任務(wù)的,當(dāng)有更新任務(wù)來的時候,不會馬上去做 Diff 操作,而是先把當(dāng)前的更新送入一個 Update Queue 中,然后交給 Scheduler 去處理,Scheduler 會根據(jù)當(dāng)前主線程的使用情況去處理這次 Update。為了實現(xiàn)這種特性,使用了requestIdelCallbackAPI。對于不支持這個API 的瀏覽器,React 會加上 pollyfill。
在上面我們已經(jīng)知道瀏覽器是一幀一幀執(zhí)行的,在兩個執(zhí)行幀之間,主線程通常會有一小段空閑時間,requestIdleCallback可以在這個空閑期(Idle Period)調(diào)用空閑期回調(diào)(Idle Callback),執(zhí)行一些任務(wù)。
低優(yōu)先級任務(wù)由requestIdleCallback處理;
高優(yōu)先級任務(wù),如動畫相關(guān)的由requestAnimationFrame處理;
requestIdleCallback 可以在多個空閑期調(diào)用空閑期回調(diào),執(zhí)行任務(wù);
requestIdleCallback 方法提供 deadline,即任務(wù)執(zhí)行限制時間,以切分任務(wù),避免長時間執(zhí)行,阻塞UI渲染而導(dǎo)致掉幀;
這個方案看似確實不錯,但是怎么實現(xiàn)可能會遇到幾個問題:
如何拆分成子任務(wù)?
一個子任務(wù)多大合適?
怎么判斷是否還有剩余時間?
有剩余時間怎么去調(diào)度應(yīng)該執(zhí)行哪一個任務(wù)?
沒有剩余時間之前的任務(wù)怎么辦?
接下里整個 Fiber 架構(gòu)就是來解決這些問題的。
什么是 Fiber為了解決之前提到解決方案遇到的問題,提出了以下幾個目標(biāo):
暫停工作,稍后再回來。
為不同類型的工作分配優(yōu)先權(quán)。
重用以前完成的工作。
如果不再需要,則中止工作。
為了做到這些,我們首先需要一種方法將任務(wù)分解為單元。從某種意義上說,這就是 Fiber,F(xiàn)iber 代表一種工作單元。
但是僅僅是分解為單元也無法做到中斷任務(wù),因為函數(shù)調(diào)用棧就是這樣,每個函數(shù)為一個工作,每個工作被稱為堆棧幀,它會一直工作,直到堆棧為空,無法中斷。
所以我們需要一種增量渲染的調(diào)度,那么就需要重新實現(xiàn)一個堆棧幀的調(diào)度,這個堆棧幀可以按照自己的調(diào)度算法執(zhí)行他們。另外由于這些堆棧是可以自己控制的,所以可以加入并發(fā)或者錯誤邊界等功能。
因此 Fiber 就是重新實現(xiàn)的堆棧幀,本質(zhì)上 Fiber 也可以理解為是一個虛擬的堆棧幀,將可中斷的任務(wù)拆分成多個子任務(wù),通過按照優(yōu)先級來自由調(diào)度子任務(wù),分段更新,從而將之前的同步渲染改為異步渲染。
所以我們可以說 Fiber 是一種數(shù)據(jù)結(jié)構(gòu)(堆棧幀),也可以說是一種解決可中斷的調(diào)用任務(wù)的一種解決方案,它的特性就是時間分片(time slicing)和暫停(supense)。
如果了解協(xié)程的可能會覺得 Fiber 的這種解決方案,跟協(xié)程有點像(區(qū)別還是很大的),是可以中斷的,可以控制執(zhí)行順序。在 JS 里的 generator 其實就是一種協(xié)程的使用方式,不過顆粒度更小,可以控制函數(shù)里面的代碼調(diào)用的順序,也可以中斷。Fiber 是如何工作的
ReactDOM.render() 和 setState 的時候開始創(chuàng)建更新。
將創(chuàng)建的更新加入任務(wù)隊列,等待調(diào)度。
在 requestIdleCallback 空閑時執(zhí)行任務(wù)。
從根節(jié)點開始遍歷 Fiber Node,并且構(gòu)建 WokeInProgress Tree。
生成 effectList。
根據(jù) EffectList 更新 DOM。
下面是一個詳細(xì)的執(zhí)行過程圖:
第一部分從 ReactDOM.render() 方法開始,把接收的 React Element 轉(zhuǎn)換為 Fiber 節(jié)點,并為其設(shè)置優(yōu)先級,創(chuàng)建 Update,加入到更新隊列,這部分主要是做一些初始數(shù)據(jù)的準(zhǔn)備。
第二部分主要是三個函數(shù):scheduleWork、requestWork、performWork,即安排工作、申請工作、正式工作三部曲,React 16 新增的異步調(diào)用的功能則在這部分實現(xiàn),這部分就是 Schedule 階段,前面介紹的 Cooperative Scheduling 就是在這個階段,只有在這個解決獲取到可執(zhí)行的時間片,第三部分才會繼續(xù)執(zhí)行。具體是如何調(diào)度的,后面文章再介紹,這是 React 調(diào)度的關(guān)鍵過程。
第三部分是一個大循環(huán),遍歷所有的 Fiber 節(jié)點,通過 Diff 算法計算所有更新工作,產(chǎn)出 EffectList 給到 commit 階段使用,這部分的核心是 beginWork 函數(shù),這部分基本就是 Fiber Reconciler ,包括 reconciliation 和 commit 階段。
Fiber NodeFIber Node,承載了非常關(guān)鍵的上下文信息,可以說是貫徹整個創(chuàng)建和更新的流程,下來分組列了一些重要的 Fiber 字段。
{ ... // 跟當(dāng)前Fiber相關(guān)本地狀態(tài)(比如瀏覽器環(huán)境就是DOM節(jié)點) stateNode: any, // 單鏈表樹結(jié)構(gòu) return: Fiber | null,// 指向他在Fiber節(jié)點樹中的`parent`,用來在處理完這個節(jié)點之后向上返回 child: Fiber | null,// 指向自己的第一個子節(jié)點 sibling: Fiber | null, // 指向自己的兄弟結(jié)構(gòu),兄弟節(jié)點的return指向同一個父節(jié)點 // 更新相關(guān) pendingProps: any, // 新的變動帶來的新的props memoizedProps: any, // 上一次渲染完成之后的props updateQueue: UpdateQueueFiber Reconciler| null, // 該Fiber對應(yīng)的組件產(chǎn)生的Update會存放在這個隊列里面 memoizedState: any, // 上一次渲染的時候的state // Scheduler 相關(guān) expirationTime: ExpirationTime, // 代表任務(wù)在未來的哪個時間點應(yīng)該被完成,不包括他的子樹產(chǎn)生的任務(wù) // 快速確定子樹中是否有不在等待的變化 childExpirationTime: ExpirationTime, // 在Fiber樹更新的過程中,每個Fiber都會有一個跟其對應(yīng)的Fiber // 我們稱他為`current <==> workInProgress` // 在渲染完成之后他們會交換位置 alternate: Fiber | null, // Effect 相關(guān)的 effectTag: SideEffectTag, // 用來記錄Side Effect nextEffect: Fiber | null, // 單鏈表用來快速查找下一個side effect firstEffect: Fiber | null, // 子樹中第一個side effect lastEffect: Fiber | null, // 子樹中最后一個side effect .... };
在第二部分,進(jìn)行 Schedule 完,獲取到時間片之后,就開始進(jìn)行 reconcile。
Fiber Reconciler 是 React 里的調(diào)和器,這也是任務(wù)調(diào)度完成之后,如何去執(zhí)行每個任務(wù),如何去更新每一個節(jié)點的過程,對應(yīng)上面的第三部分。
reconcile 過程分為2個階段(phase):
(可中斷)render/reconciliation 通過構(gòu)造 WorkInProgress Tree 得出 Change。
(不可中斷)commit 應(yīng)用這些DOM change。
reconciliation 階段在 reconciliation 階段的每個工作循環(huán)中,每次處理一個 Fiber,處理完可以中斷/掛起整個工作循環(huán)。通過每個節(jié)點更新結(jié)束時向上歸并 Effect List 來收集任務(wù)結(jié)果,reconciliation 結(jié)束后,根節(jié)點的 Effect List里記錄了包括 DOM change 在內(nèi)的所有 Side Effect。
render 階段可以理解為就是 Diff 的過程,得出 Change(Effect List),會執(zhí)行聲明如下的聲明周期方法:
[UNSAFE_]componentWillMount(棄用)
[UNSAFE_]componentWillReceiveProps(棄用)
getDerivedStateFromProps
shouldComponentUpdate
[UNSAFE_]componentWillUpdate(棄用)
render
由于 reconciliation 階段是可中斷的,一旦中斷之后恢復(fù)的時候又會重新執(zhí)行,所以很可能 reconciliation 階段的生命周期方法會被多次調(diào)用,所以在 reconciliation 階段的生命周期的方法是不穩(wěn)定的,我想這也是 React 為什么要廢棄 componentWillMount 和 componentWillReceiveProps方法而改為靜態(tài)方法 getDerivedStateFromProps 的原因吧。
commit 階段commit 階段可以理解為就是將 Diff 的結(jié)果反映到真實 DOM 的過程。
在 commit 階段,在 commitRoot 里會根據(jù) effect 的 effectTag,具體 effectTag 見源碼 ,進(jìn)行對應(yīng)的插入、更新、刪除操作,根據(jù) tag 不同,調(diào)用不同的更新方法。
commit 階段會執(zhí)行如下的聲明周期方法:
getSnapshotBeforeUpdate
componentDidMount
componentDidUpdate
componentWillUnmount
P.S:注意區(qū)別 reconciler、reconcile 和 reconciliation,reconciler 是調(diào)和器,是一個名詞,可以說是 React 工作的一個模塊,協(xié)調(diào)模塊;reconcile 是調(diào)和器調(diào)和的動作,是一個動詞;而 reconciliation 只是 reconcile 過程的第一個階段。Fiber Tree 和 WorkInProgress Tree
React 在 render 第一次渲染時,會通過 React.createElement 創(chuàng)建一顆 Element 樹,可以稱之為 Virtual DOM Tree,由于要記錄上下文信息,加入了 Fiber,每一個 Element 會對應(yīng)一個 Fiber Node,將 Fiber Node 鏈接起來的結(jié)構(gòu)成為 Fiber Tree。它反映了用于渲染 UI 的應(yīng)用程序的狀態(tài)。這棵樹通常被稱為 current 樹(當(dāng)前樹,記錄當(dāng)前頁面的狀態(tài))。
在后續(xù)的更新過程中(setState),每次重新渲染都會重新創(chuàng)建 Element, 但是 Fiber 不會,F(xiàn)iber 只會使用對應(yīng)的 Element 中的數(shù)據(jù)來更新自己必要的屬性,
Fiber Tree 一個重要的特點是鏈表結(jié)構(gòu),將遞歸遍歷編程循環(huán)遍歷,然后配合 requestIdleCallback API, 實現(xiàn)任務(wù)拆分、中斷與恢復(fù)。
這個鏈接的結(jié)構(gòu)是怎么構(gòu)成的呢,這就要主要到之前 Fiber Node 的節(jié)點的這幾個字段:
// 單鏈表樹結(jié)構(gòu) { return: Fiber | null, // 指向父節(jié)點 child: Fiber | null,// 指向自己的第一個子節(jié)點 sibling: Fiber | null,// 指向自己的兄弟結(jié)構(gòu),兄弟節(jié)點的return指向同一個父節(jié)點 }
每一個 Fiber Node 節(jié)點與 Virtual Dom 一一對應(yīng),所有 Fiber Node 連接起來形成 Fiber tree, 是個單鏈表樹結(jié)構(gòu),如下圖所示:
對照圖來看,是不是可以知道 Fiber Node 是如何聯(lián)系起來的呢,F(xiàn)iber Tree 就是這樣一個單鏈表。
當(dāng) render 的時候有了這么一條單鏈表,當(dāng)調(diào)用 setState 的時候又是如何 Diff 得到 change 的呢?
采用的是一種叫雙緩沖技術(shù)(double buffering),這個時候就需要另外一顆樹:WorkInProgress Tree,它反映了要刷新到屏幕的未來狀態(tài)。
WorkInProgress Tree 構(gòu)造完畢,得到的就是新的 Fiber Tree,然后喜新厭舊(把 current 指針指向WorkInProgress Tree,丟掉舊的 Fiber Tree)就好了。
這樣做的好處:
能夠復(fù)用內(nèi)部對象(fiber)
節(jié)省內(nèi)存分配、GC的時間開銷
就算運行中有錯誤,也不會影響 View 上的數(shù)據(jù)
每個 Fiber上都有個alternate屬性,也指向一個 Fiber,創(chuàng)建 WorkInProgress 節(jié)點時優(yōu)先取alternate,沒有的話就創(chuàng)建一個。
創(chuàng)建 WorkInProgress Tree 的過程也是一個 Diff 的過程,Diff 完成之后會生成一個 Effect List,這個 Effect List 就是最終 Commit 階段用來處理副作用的階段。
后記本開始想一篇文章把 Fiber 講透的,但是寫著寫著發(fā)現(xiàn)確實太多了,想寫詳細(xì),估計要寫幾萬字,所以我這篇文章的目的僅僅是在沒有涉及到源碼的情況下梳理了大致 React 的工作流程,對于細(xì)節(jié),比如如何調(diào)度異步任務(wù)、如何去做 Diff 等等細(xì)節(jié)將以小節(jié)的方式一個個的結(jié)合源碼進(jìn)行分析。
說實話,自己不是特別滿意這篇,感覺頭重腳輕,在講協(xié)調(diào)之前寫得還挺好的,但是在講協(xié)調(diào)這塊文字反而變少了,因為我是專門想寫一篇文章講協(xié)調(diào)的,所以這篇僅僅用來梳理整個流程。
但是梳理整個流程又發(fā)現(xiàn) Schedule 這塊基本沒什么體現(xiàn),哎,不想寫了,這篇文章拖太久了,請繼續(xù)后續(xù)的文章。
可以關(guān)注我的 github:Deep In React
一些問題接下來留一些思考題。
如何去劃分任務(wù)優(yōu)先級?
在 reconcile 過程的 render 階段是如何去遍歷鏈表,如何去構(gòu)建 workInProgress 的?
當(dāng)任務(wù)被打斷,如何恢復(fù)?
如何去收集 EffectList?
針對不同的組件類型如何進(jìn)行更新?
參考完全理解 React Fiber)
Fiber
React16源碼之React Fiber架構(gòu)
我是桃翁,一個愛思考的前端er,想了解關(guān)于更多的前端相關(guān)的,請關(guān)注我的公號:「前端桃園」
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/105036.html
摘要:因為版本將真正廢棄這三生命周期到目前為止,的渲染機(jī)制遵循同步渲染首次渲染,更新時更新時卸載時期間每個周期函數(shù)各司其職,輸入輸出都是可預(yù)測,一路下來很順暢。通過進(jìn)一步觀察可以發(fā)現(xiàn),預(yù)廢棄的三個生命周期函數(shù)都發(fā)生在虛擬的構(gòu)建期間,也就是之前。 showImg(https://segmentfault.com/img/bVbweoj?w=559&h=300); 背景 前段時間準(zhǔn)備前端招聘事項...
摘要:毫無疑問,作為近兩年前端三大流行框架之一,正成為程序員們最喜愛的框架。自年月開源至今,已有千萬網(wǎng)站使用來進(jìn)行前端構(gòu)架,使之成為最受歡迎的項目之一。經(jīng)過這幾年的沉淀,越來越強大,暫不提這幾年在國內(nèi)與之間的矛盾,這其中還有很大談判空間。 showImg(https://segmentfault.com/img/bV0dY9?w=469&h=240); React在國外已被各個公司的各種產(chǎn)品...
摘要:對于同一層級的一組子節(jié)點,它們可以通過唯一進(jìn)行區(qū)分。基于以上三個前提策略,分別對以及進(jìn)行算法優(yōu)化。鏈表的每一個節(jié)點是,而不是在之前的虛擬節(jié)點。是當(dāng)前層的第一個節(jié)點。再次提醒,是的一層。 文章首發(fā)于個人博客 這是我 Deep In React 系列的第二篇文章,如果還沒有讀過的強烈建議你先讀第一篇:詳談 React Fiber 架構(gòu)(1)。 前言 我相信在看這篇文章的讀者一般都已經(jīng)了解...
摘要:架構(gòu)理解引用原文是核心算法正在進(jìn)行的重新實現(xiàn)。構(gòu)建的過程就是的過程,通過來調(diào)度執(zhí)行一組任務(wù),每完成一個任務(wù)后回來看看有沒有插隊的更緊急的,把時間控制權(quán)交還給主線程,直到下一次回調(diào)再繼續(xù)構(gòu)建。 React Fiber 架構(gòu)理解 引用原文:React Fiber ArchitectureReact Fiber is an ongoing reimplementation of Reacts...
摘要:什么是每一個都有一個對應(yīng)的,記錄這個節(jié)點的各種狀態(tài),是一鏈表的結(jié)構(gòu)的串聯(lián)起來。 1. 什么是fiber 每一個ReactElement都有一個對應(yīng)的fiber, 記錄這個節(jié)點的各種狀態(tài), fiber是一鏈表的結(jié)構(gòu)的串聯(lián)起來。showImg(https://segmentfault.com/img/bVbqVZR?w=540&h=708); 2. Fiber的組成 export type...
閱讀 3462·2021-11-25 09:43
閱讀 1062·2021-11-15 11:36
閱讀 3313·2021-11-11 16:54
閱讀 3974·2021-09-27 13:35
閱讀 4364·2021-09-10 11:23
閱讀 5675·2021-09-07 10:22
閱讀 3032·2021-09-04 16:40
閱讀 769·2021-08-03 14:03