文章首發於我的博客javascript
2016 年都已經透露出來的概念,這都 9102 年了,我纔開始寫 Fiber 的文章,表示慚愧呀。不過如今好的是關於 Fiber 的資料已經很豐富了,在寫文章的時候參考資料比較多,比較容易深入的理解。html
React 做爲我最喜歡的框架,沒有之一,我願意花不少時間來好好的學習他,我發現對於學習一門框架會有四種感覺,剛開始沒使用過,可能有一種很神奇的感受;而後接觸了,遇到了不熟悉的語法,感受這是什麼垃圾東西,這不是反人類麼;而後當你熟悉了以後,真香,設計得挺好的,這個時候它已經改變了你編程的思惟方式了;再到後來,看過他的源碼,理解他的設計以後,設計得確實好,感受本身也能寫一個的樣子。前端
因此我今年(對,沒錯,就是一年)就是想徹底的學透 React,因此開了一個 Deep In React 的系列,把一些新手在使用 API 的時候不知道爲何的點,以及一些爲何有些東西要這麼設計寫出來,與你們共同探討 React 的奧祕。java
個人思路是自上而下的介紹,先理解總體的 Fiber 架構,而後再細挖每個點,因此這篇文章主要是談 Fiber 架構的。react
在詳細介紹 Fiber 以前,先了解一下 Fiber 是什麼,以及爲何 React 團隊要話兩年時間重構協調算法。git
內存中維護一顆虛擬DOM樹,數據變化時(setState),自動更新虛擬 DOM,獲得一顆新樹,而後 Diff 新老虛擬 DOM 樹,找到有變化的部分,獲得一個 Change(Patch),將這個 Patch 加入隊列,最終批量更新這些 Patch 到 DOM 中。github
首先咱們瞭解一下 React 的工做過程,當咱們經過render()
和 setState()
進行組件渲染和更新的時候,React 主要有兩個階段:算法
調和階段(Reconciler):官方解釋。React 會自頂向下經過遞歸,遍歷新數據生成新的 Virtual DOM,而後經過 Diff 算法,找到須要變動的元素(Patch),放到更新隊列裏面去。編程
渲染階段(Renderer):遍歷更新隊列,經過調用宿主環境的API,實際更新渲染對應元素。宿主環境,好比 DOM、Native、WebGL 等。瀏覽器
在協調階段階段,因爲是採用的遞歸的遍歷方式,這種也被成爲 Stack Reconciler,主要是爲了區別 Fiber Reconciler 取的一個名字。這種方式有一個特色:一旦任務開始進行,就沒法中斷,那麼 js 將一直佔用主線程, 一直要等到整棵 Virtual DOM 樹計算完成以後,才能把執行權交給渲染引擎,那麼這就會致使一些用戶交互、動畫等任務沒法當即獲得處理,就會有卡頓,很是的影響用戶體驗。
以前的問題主要的問題是任務一旦執行,就沒法中斷,js 線程一直佔用主線程,致使卡頓。
可能有些接觸前端不久的不是特別理解上面爲何 js 一直佔用主線程就會卡頓,我這裏仍是簡單的普及一下。
頁面是一幀一幀繪製出來的,當每秒繪製的幀數(FPS)達到 60 時,頁面是流暢的,小於這個值時,用戶會感受到卡頓。
1s 60 幀,因此每一幀分到的時間是 1000/60 ≈ 16 ms。因此咱們書寫代碼時力求不讓一幀的工做量超過 16ms。
瀏覽器一幀內的工做
經過上圖可看到,一幀內須要完成以下六個步驟的任務:
若是這六個步驟中,任意一個步驟所佔用的時間過長,總時間超過 16ms 了以後,用戶也許就能看到卡頓。
而在上一小節提到的調和階段花的時間過長,也就是 js 執行的時間過長,那麼就有可能在用戶有交互的時候,原本應該是渲染下一幀了,可是在當前一幀裏還在執行 JS,就致使用戶交互不能麻煩獲得反饋,從而產生卡頓感。
把渲染更新過程拆分紅多個子任務,每次只作一小部分,作完看是否還有剩餘時間,若是有繼續下一個任務;若是沒有,掛起當前任務,將時間控制權交給主線程,等主線程不忙的時候在繼續執行。 這種策略叫作 Cooperative Scheduling(合做式調度),操做系統經常使用任務調度策略之一。
補充知識,操做系統經常使用任務調度策略:先來先服務(FCFS)調度算法、短做業(進程)優先調度算法(SJ/PF)、最高優先權優先調度算法(FPF)、高響應比優先調度算法(HRN)、時間片輪轉法(RR)、多級隊列反饋法。
合做式調度主要就是用來分配任務的,當有更新任務來的時候,不會立刻去作 Diff 操做,而是先把當前的更新送入一個 Update Queue 中,而後交給 Scheduler 去處理,Scheduler 會根據當前主線程的使用狀況去處理此次 Update。爲了實現這種特性,使用了requestIdelCallback
API。對於不支持這個API 的瀏覽器,React 會加上 pollyfill。
在上面咱們已經知道瀏覽器是一幀一幀執行的,在兩個執行幀之間,主線程一般會有一小段空閒時間,requestIdleCallback
能夠在這個空閒期(Idle Period)調用空閒期回調(Idle Callback),執行一些任務。
requestIdleCallback
處理;requestAnimationFrame
處理;requestIdleCallback
能夠在多個空閒期調用空閒期回調,執行任務;requestIdleCallback
方法提供 deadline,即任務執行限制時間,以切分任務,避免長時間執行,阻塞UI渲染而致使掉幀;這個方案看似確實不錯,可是怎麼實現可能會遇到幾個問題:
接下里整個 Fiber 架構就是來解決這些問題的。
爲了解決以前提到解決方案遇到的問題,提出瞭如下幾個目標:
爲了作到這些,咱們首先須要一種方法將任務分解爲單元。從某種意義上說,這就是 Fiber,Fiber 表明一種工做單元。
可是僅僅是分解爲單元也沒法作到中斷任務,由於函數調用棧就是這樣,每一個函數爲一個工做,每一個工做被稱爲堆棧幀,它會一直工做,直到堆棧爲空,沒法中斷。
因此咱們須要一種增量渲染的調度,那麼就須要從新實現一個堆棧幀的調度,這個堆棧幀能夠按照本身的調度算法執行他們。另外因爲這些堆棧是能夠本身控制的,因此能夠加入併發或者錯誤邊界等功能。
所以 Fiber 就是從新實現的堆棧幀,本質上 Fiber 也能夠理解爲是一個虛擬的堆棧幀,將可中斷的任務拆分紅多個子任務,經過按照優先級來自由調度子任務,分段更新,從而將以前的同步渲染改成異步渲染。
因此咱們能夠說 Fiber 是一種數據結構(堆棧幀),也能夠說是一種解決可中斷的調用任務的一種解決方案,它的特性就是時間分片(time slicing)和暫停(supense)。
若是瞭解協程的可能會以爲 Fiber 的這種解決方案,跟協程有點像(區別仍是很大的),是能夠中斷的,能夠控制執行順序。在 JS 裏的 generator 其實就是一種協程的使用方式,不過顆粒度更小,能夠控制函數裏面的代碼調用的順序,也能夠中斷。
ReactDOM.render()
和 setState
的時候開始建立更新。下面是一個詳細的執行過程圖:
ReactDOM.render()
方法開始,把接收的 React Element 轉換爲 Fiber 節點,併爲其設置優先級,建立 Update,加入到更新隊列,這部分主要是作一些初始數據的準備。scheduleWork
、requestWork
、performWork
,即安排工做、申請工做、正式工做三部曲,React 16 新增的異步調用的功能則在這部分實現,這部分就是 Schedule 階段,前面介紹的 Cooperative Scheduling 就是在這個階段,只有在這個解決獲取到可執行的時間片,第三部分纔會繼續執行。具體是如何調度的,後面文章再介紹,這是 React 調度的關鍵過程。FIber Node,承載了很是關鍵的上下文信息,能夠說是貫徹整個建立和更新的流程,下來分組列了一些重要的 Fiber 字段。
{
...
// 跟當前Fiber相關本地狀態(好比瀏覽器環境就是DOM節點)
stateNode: any,
// 單鏈表樹結構
return: Fiber | null,// 指向他在Fiber節點樹中的`parent`,用來在處理完這個節點以後向上返回
child: Fiber | null,// 指向本身的第一個子節點
sibling: Fiber | null, // 指向本身的兄弟結構,兄弟節點的return指向同一個父節點
// 更新相關
pendingProps: any, // 新的變更帶來的新的props
memoizedProps: any, // 上一次渲染完成以後的props
updateQueue: UpdateQueue<any> | null, // 該Fiber對應的組件產生的Update會存放在這個隊列裏面
memoizedState: any, // 上一次渲染的時候的state
// Scheduler 相關
expirationTime: ExpirationTime, // 表明任務在將來的哪一個時間點應該被完成,不包括他的子樹產生的任務
// 快速肯定子樹中是否有不在等待的變化
childExpirationTime: ExpirationTime,
// 在Fiber樹更新的過程當中,每一個Fiber都會有一個跟其對應的Fiber
// 咱們稱他爲`current <==> workInProgress`
// 在渲染完成以後他們會交換位置
alternate: Fiber | null,
// Effect 相關的
effectTag: SideEffectTag, // 用來記錄Side Effect
nextEffect: Fiber | null, // 單鏈表用來快速查找下一個side effect
firstEffect: Fiber | null, // 子樹中第一個side effect
lastEffect: Fiber | null, // 子樹中最後一個side effect
....
};
複製代碼
在第二部分,進行 Schedule 完,獲取到時間片以後,就開始進行 reconcile。
Fiber Reconciler 是 React 裏的調和器,這也是任務調度完成以後,如何去執行每一個任務,如何去更新每個節點的過程,對應上面的第三部分。
reconcile 過程分爲2個階段(phase):
在 reconciliation 階段的每一個工做循環中,每次處理一個 Fiber,處理完能夠中斷/掛起整個工做循環。經過每一個節點更新結束時向上歸併 Effect List 來收集任務結果,reconciliation 結束後,根節點的 Effect List裏記錄了包括 DOM change 在內的全部 Side Effect。
render 階段能夠理解爲就是 Diff 的過程,得出 Change(Effect List),會執行聲明以下的聲明週期方法:
因爲 reconciliation 階段是可中斷的,一旦中斷以後恢復的時候又會從新執行,因此極可能 reconciliation 階段的生命週期方法會被屢次調用,因此在 reconciliation 階段的生命週期的方法是不穩定的,我想這也是 React 爲何要廢棄 componentWillMount
和 componentWillReceiveProps
方法而改成靜態方法 getDerivedStateFromProps
的緣由吧。
commit 階段能夠理解爲就是將 Diff 的結果反映到真實 DOM 的過程。
在 commit 階段,在 commitRoot 裏會根據 effect
的 effectTag
,具體 effectTag 見源碼 ,進行對應的插入、更新、刪除操做,根據 tag
不一樣,調用不一樣的更新方法。
commit 階段會執行以下的聲明週期方法:
P.S:注意區別 reconciler、reconcile 和 reconciliation,reconciler 是調和器,是一個名詞,能夠說是 React 工做的一個模塊,協調模塊;reconcile 是調和器調和的動做,是一個動詞;而 reconciliation 只是 reconcile 過程的第一個階段。
React 在 render 第一次渲染時,會經過 React.createElement 建立一顆 Element 樹,能夠稱之爲 Virtual DOM Tree,因爲要記錄上下文信息,加入了 Fiber,每個 Element 會對應一個 Fiber Node,將 Fiber Node 連接起來的結構成爲 Fiber Tree。它反映了用於渲染 UI 的應用程序的狀態。這棵樹一般被稱爲 current 樹(當前樹,記錄當前頁面的狀態)。
在後續的更新過程當中(setState),每次從新渲染都會從新建立 Element, 可是 Fiber 不會,Fiber 只會使用對應的 Element 中的數據來更新本身必要的屬性,
Fiber Tree 一個重要的特色是鏈表結構,將遞歸遍歷編程循環遍歷,而後配合 requestIdleCallback API, 實現任務拆分、中斷與恢復。
這個連接的結構是怎麼構成的呢,這就要主要到以前 Fiber Node 的節點的這幾個字段:
// 單鏈表樹結構
{
return: Fiber | null, // 指向父節點
child: Fiber | null,// 指向本身的第一個子節點
sibling: Fiber | null,// 指向本身的兄弟結構,兄弟節點的return指向同一個父節點
}
複製代碼
每個 Fiber Node 節點與 Virtual Dom 一一對應,全部 Fiber Node 鏈接起來造成 Fiber tree, 是個單鏈表樹結構,以下圖所示:
對照圖來看,是否是能夠知道 Fiber Node 是如何聯繫起來的呢,Fiber Tree 就是這樣一個單鏈表。
當 render 的時候有了這麼一條單鏈表,當調用 setState
的時候又是如何 Diff 獲得 change 的呢?
採用的是一種叫雙緩衝技術(double buffering),這個時候就須要另一顆樹:WorkInProgress Tree,它反映了要刷新到屏幕的將來狀態。
WorkInProgress Tree 構造完畢,獲得的就是新的 Fiber Tree,而後喜新厭舊(把 current 指針指向WorkInProgress Tree,丟掉舊的 Fiber Tree)就行了。
這樣作的好處:
每一個 Fiber上都有個alternate
屬性,也指向一個 Fiber,建立 WorkInProgress 節點時優先取alternate
,沒有的話就建立一個。
建立 WorkInProgress Tree 的過程也是一個 Diff 的過程,Diff 完成以後會生成一個 Effect List,這個 Effect List 就是最終 Commit 階段用來處理反作用的階段。
本開始想一篇文章把 Fiber 講透的,可是寫着寫着發現確實太多了,想寫詳細,估計要寫幾萬字,因此我這篇文章的目的僅僅是在沒有涉及到源碼的狀況下梳理了大體 React 的工做流程,對於細節,好比如何調度異步任務、如何去作 Diff 等等細節將以小節的方式一個個的結合源碼進行分析。
說實話,本身不是特別滿意這篇,感受頭重腳輕,在講協調以前寫得還挺好的,可是在講協調這塊文字反而變少了,由於我是專門想寫一篇文章講協調的,因此這篇僅僅用來梳理整個流程。
可是梳理整個流程又發現 Schedule 這塊基本沒什麼體現,哎,不想寫了,這篇文章拖過久了,請繼續後續的文章。
能夠關注個人 github:Deep In React
接下來留一些思考題。
我是桃翁,一個愛思考的前端er,想了解關於更多的前端相關的,請關注個人公號:「前端桃園」