React 重要的一次重構:認識非同步渲染架構 Fiber

創宇前端發表於2018-11-15

Diff 演算法

熟悉 react 的朋友都知道,在 react 中有個核心的演算法,叫 diff 演算法。web 介面由 dom 樹組成,不同的 dom 樹會渲染出不同的介面。react 使用 virtual dom 來表示 dom 樹,而 diff 演算法就是用於比較 virtual dom 樹的區別,並更新介面需要更新的部分。diff 演算法和 virtual dom 的完美結合的過程被稱為 reconciler,這可是 react 攻城拔寨的絕對利器。有了 reconciler,開發者可以脫身操作真實的 dom 樹,只需要向 react 描述介面的狀態,而 react 會幫助你高效的完成真正 dom 操作。

React 重要的一次重構:認識非同步渲染架構 Fiber
在 react16 之前的 reconciler 叫 stack reconciler,fiber 是 react 新的 reconciler,這次更新到 fiber 架構是一次重量級的核心架構的替換,react 為了完成這次替換已經準備了兩三年的時間了。

那麼 fiber 究竟有什麼好的呢?

Fiber 為何出現

不知道大家有沒有遇到過這樣的情況,點選一個頁面的按鈕時感覺到頁面沒有任何的反應,讓你懷疑電腦是不是當機了,然後你快速切出瀏覽器,發現電腦並沒有當機,於是再切回瀏覽器,這時候才發現頁面終於更新了。為什麼會出現這種情況?在多數情況下,可能是因為瀏覽器忙著執行相關的 js 程式碼,導致瀏覽器主執行緒沒有及時響應使用者的操作或者沒有及時更新介面。下面這張圖就表示了這種現象,你的公司只有一個程式設計師 (main thread),當這個程式設計師在執行你的任務 (your code) 時,處於沉浸式程式設計的狀態,無法響應外部的其他事件,什麼下班吃飯,都是不存在的。這就像瀏覽器忙著執行 js 程式碼的時候,不會去執行頁面更新等操作。

React 重要的一次重構:認識非同步渲染架構 Fiber
本著顧客是上帝的原則,作為一名優秀的開發者,怎麼能夠允許出現這種情況降低使用者的體驗呢。因此 react 團隊引入了非同步渲染這個概念,而採用 fiber 架構可以實現這種非同步渲染的方式。

原先的 stack reconciler 像是一個遞迴執行的函式,從父元件呼叫子元件的 reconciler 過程就是一個遞迴執行的過程,這也是為什麼被稱為 stack reconciler 的原因。當我們呼叫 setState 的時候,react 從根節點開始遍歷,找出所有的不同,而對於特別龐大的 dom 樹來說,這個遞迴遍歷的過程會消耗特別長的時間。在這個期間,任何互動和渲染都會被阻塞,這樣就給使用者一種“當機”的感覺。

React 重要的一次重構:認識非同步渲染架構 Fiber
fiber 的出現解決了這個問題,它把 reconciler 的過程拆分成了一個個的小任務,並在完成了小任務之後暫停執行 js 程式碼,然後檢查是否有需要更新的內容和需要響應的事件,做出相應的處理後再繼續執行 js 程式碼。這樣就給了使用者一種應用一直在執行的感覺,提高了使用者的體驗。

React 重要的一次重構:認識非同步渲染架構 Fiber

Fiber 如何做到非同步渲染

在做顯示方面的工作時,經常會聽到一個目標叫 60 幀,這表示的是畫面的更新頻率,也就是畫面每秒鐘更新 60 次。這是因為在 60 幀的更新頻率下,頁面在人眼中顯得流暢,無明顯示卡頓。每秒鐘更新 60 次也就是每 16ms 需要更新一次頁面,如果更新頁面消耗的時間不到 16ms,那麼在下一次更新時機來到之前會剩下一點時間執行其他的任務,只要保證及時在 16ms 的間隔下更新介面就完全不會影響到頁面的流暢程度。fiber 的核心正是利用了 60 幀原則,實現了一個基於優先順序和 requestIdleCallback 的迴圈任務排程演算法。

React 重要的一次重構:認識非同步渲染架構 Fiber
requestIdleCallback 是瀏覽器提供的一個 api,可以讓瀏覽器在空閒的時候執行回撥,在回撥引數中可以獲取到當前幀剩餘的時間,fiber 利用了這個引數,判斷當前剩下的時間是否足夠繼續執行任務,如果足夠則繼續執行,否則暫停任務,並呼叫 requestIdleCallback 通知瀏覽器空閒的時候繼續執行當前的任務。

function fiber(剩餘時間) {
 if (剩餘時間 > 任務所需時間) {
 做任務;
 } else {
 requestIdleCallback(fiber);
 }
}
複製程式碼

fiber 還會為不同的任務設定不同的優先順序,高優先順序任務是需要馬上展示到頁面上的,比如你正在輸入框中輸入文字,你肯定希望你的手指在鍵盤上敲下每一個按鍵時,輸入框能立馬做出反饋,這樣你才能知道你的輸入是否正確,是否有效。低優先順序的任務則是像從伺服器傳來了一些資料,這個時候需要更新頁面,比如這篇文章喜歡的人數+1 或是評論+1,這並不是那麼緊急的更新,延遲 100-200ms 並不會有多大差別,完全可以在後面進行處理。fiber 會根據任務優先順序來動態調整任務排程,優先完成高優先順序的任務。

{ 
 Synchronous: 1, // 同步任務,優先順序最高
 Task: 2, // 當前排程正執行的任務
 Animation 3, // 動畫
 High: 4, // 高優先順序
 Low: 5, // 低優先順序
 Offscreen: 6, // 當前螢幕外的更新,優先順序最低
}
複製程式碼

在 fiber 架構中,有一種資料結構,它的名字就叫做 fiber,這也是為什麼新的 reconciler 叫做 fiber 的原因。fiber 其實就是一個 js 物件,這個物件的屬性中比較重要的有 stateNodetagreturnchildsiblingalternate

Fiber = {
 tag // 標記任務的進度
 return // 父節點
 child // 子節點
 sibling // 兄弟節點
 alternate // 變化記錄
 .....
};
複製程式碼

我們可以看出 fiber 基於連結串列結構,擁有一個個指標,指向它的父節點子節點和兄弟節點,在 diff 的過程中,依照節點連線的關係進行遍歷。

fiber 可能存在的問題

在 fiber 中,更新是分階段的,具體分為兩個階段,首先是 reconciliation 的階段,這個階段在計算前後 dom 樹的差異,然後是 commit 的階段,這個階段將把更新渲染到頁面上。第一個階段是可以打斷的,因為這個階段耗時可能會很長,因此需要暫停下來去執行其他更高優先順序的任務,第二個階段則不會被打斷,會一口氣把更新渲染到頁面上。

React 重要的一次重構:認識非同步渲染架構 Fiber
由於 reconciliation 的階段會被打斷,可能會導致 commit 前的這些生命週期函式多次執行。react 官方目前已經把 componentWillMountcomponentWillReceivePropscomponetWillUpdate 標記為 unsafe,並使用新的生命週期函式 getDerivedStateFromPropsgetSnapshotBeforeUpdate 進行替換。

還有一個問題是飢餓問題,意思是如果高優先順序的任務一直插入,導致低優先順序的任務無法得到機會執行,這被稱為飢餓問題。對於這個問題官方提出的解決方案是儘量複用已經完成的操作來緩解。相信官方也正在努力提出更好的方法去解決這個問題。


文 / Xss

編 / 熒聲

本文已由作者授權釋出,版權屬於創宇前端。歡迎註明出處轉載本文。本文連結:knownsec-fed.com/2018-10-23-…

想要訂閱更多來自知道創宇開發一線的分享,請搜尋關注我們的微信公眾號:創宇前端(KnownsecFED)。歡迎留言討論,我們會盡可能回覆。

React 重要的一次重構:認識非同步渲染架構 Fiber

感謝您的閱讀。

相關文章