react在進行組件渲染時,從setState開始到渲染完成整個過程是同步的(「一鼓作氣」)。若是須要渲染的組件比較龐大,js執行會佔據主線程時間較長,會致使頁面響應度變差,使得react在動畫、手勢等應用中效果比較差。html
爲了解決這個問題,react團隊通過兩年的工做,重寫了react中核心算法——reconciliation。並在v16版本中發佈了這個新的特性。爲了區別以前和以後的reconciler,一般將以前的reconciler稱爲stack reconciler,重寫後的稱爲fiber reconciler,簡稱爲Fiber。react
Stack reconciler的工做流程很像函數的調用過程。父組件裏調子組件,能夠類比爲函數的遞歸(這也是爲何被稱爲stack reconciler的緣由)。在setState後,react會當即開始reconciliation過程,從父節點(Virtual DOM)開始遍歷,以找出不一樣。將全部的Virtual DOM遍歷完成後,reconciler才能給出當前須要修改真實DOM的信息,並傳遞給renderer,進行渲染,而後屏幕上纔會顯示這次更新內容。對於特別龐大的vDOM樹來講,reconciliation過程會很長(x00ms),在這期間,主線程是被js佔用的,所以任何交互、佈局、渲染都會中止,給用戶的感受就是頁面被卡住了。算法
scheduling(調度)是fiber reconciliation的一個過程,主要決定應該在什麼時候作什麼。👆的過程代表在stack reconciler中,reconciliation是「一鼓作氣」,對於函數來講,這沒什麼問題,由於咱們只想要函數的運行結果,但對於UI來講還須要考慮如下問題:瀏覽器
因此理想情況下reconciliation的過程應該是像下圖所示同樣,每次只作一個很小的任務,作完後可以「喘口氣兒」,回到主線程看下有沒有什麼更高優先級的任務須要處理,若是又則先處理更高優先級的任務,沒有則繼續執行(cooperative scheduling 合做式調度)。markdown
先看一下stack-reconciler下的react是怎麼工做的。代碼中建立(或更新)一些元素,react會根據這些元素建立(或更新)Virtual DOM,而後react根據更新先後virtual DOM的區別,去修改真正的DOM。注意,在stack reconciler下,DOM的更新是同步的,也就是說,在virtual DOM的比對過程當中,發現一個instance有更新,會當即執行DOM操做。ide
而fiber-conciler下,操做是能夠分紅不少小部分,而且能夠被中斷的,因此同步操做DOM可能會致使fiber-tree與實際DOM的不一樣步。對於每一個節點來講,其不光存儲了對應元素的基本信息,還要保存一些用於任務調度的信息。所以,fiber僅僅是一個對象,表徵reconciliation階段所能拆分的最小工做單元,和上圖中的react instance一一對應。經過stateNode
屬性管理Instance自身的特性。經過child和sibling表徵當前工做單元的下一個工做單元,return表示處理完成後返回結果所要合併的目標,一般指向父節點。整個結構是一個鏈表樹。每一個工做單元(fiber)執行完成後,都會查看是否還繼續擁有主線程時間片,若是有繼續下一個,若是沒有則先處理其餘高優先級事務,等主線程空閒下來繼續執行。函數
fiber { stateNode: {}, child: {}, return: {}, sibling: {}, } 複製代碼
當前頁面包含一個列表,經過該列表渲染出一個button和一組Item,Item中包含一個div,其中的內容爲數字。經過點擊button,可使列表中的全部數字進行平方。另外有一個按鈕,點擊能夠調節字體大小。oop
頁面渲染完成後,就會初始化生成一個fiber-tree。初始化fiber-tree和初始化Virtual DOM tree沒什麼區別,這裏就再也不贅述。佈局
於此同時,react還會維護一個workInProgressTree。workInProgressTree用於計算更新,完成reconciliation過程。字體
用戶點擊平方按鈕後,利用各個元素平方後的list調用setState,react會把當前的更新送入list組件對應的update queue中。可是react並不會當即執行對比並修改DOM的操做。而是交給scheduler去處理。
scheduler會根據當前主線程的使用狀況去處理此次update。爲了實現這種特性,使用了requestIdelCallback
API。對於不支持這個API的瀏覽器,react會加上pollyfill。
總的來說,一般,客戶端線程執行任務時會以幀的形式劃分,大部分設備控制在30-60幀是不會影響用戶體驗;在兩個執行幀之間,主線程一般會有一小段空閒時間,requestIdleCallback
能夠在這個空閒期(Idle Period)調用空閒期回調(Idle Callback),執行一些任務
requestIdleCallback
處理;requestAnimationFrame
處理;requestIdleCallback
能夠在多個空閒期調用空閒期回調,執行任務;requestIdleCallback
方法提供deadline,即任務執行限制時間,以切分任務,避免長時間執行,阻塞UI渲染而致使掉幀;一旦reconciliation過程獲得時間片,就開始進入work loop。work loop機制可讓react在計算狀態和等待狀態之間進行切換。爲了達到這個目的,對於每一個loop而言,須要追蹤兩個東西:下一個工做單元(下一個待處理的fiber);當前還能佔用主線程的時間。第一個loop,下一個待處理單元爲根節點。
由於根節點上的更新隊列爲空,因此直接從fiber-tree上將根節點複製到workInProgressTree中去。根節點中包含指向子節點(List)的指針。
根節點沒有什麼更新操做,根據其child指針,接下來把List節點及其對應的update queue也複製到workinprogress中。List插入後,向其父節點返回,標誌根節點的處理完成。
根節點處理完成後,react此時檢查時間片是否用完。若是沒有用完,根據其保存的下個工做單元的信息開始處理下一個節點List。
接下來進入處理List的work loop,List中包含更新,所以此時react會調用setState時傳入的updater funciton獲取最新的state值,此時應該是[1,4,9]。一般咱們如今在調用setState傳入的是一個對象,但在使用fiber conciler時,必須傳入一個函數,函數的返回值是要更新的state。react從很早的版本就開始支持這種寫法了,不過一般沒有人用。在以後的react版本中,可能會廢棄直接傳入對象的寫法。
setState({}, callback); // stack conciler setState(() => { return {} }, callback); // fiber conciler 複製代碼
在獲取到最新的state值後,react會更新List的state和props值,而後調用render,而後獲得一組經過更新後的list值生成的elements。react會根據生成elements的類型,來決定fiber是否可重用。對於當前狀況來講,新生成的elments類型並無變(依然是Button和Item),因此react會直接從fiber-tree中複製這些elements對應的fiber到workInProgress 中。並給List打上標籤,由於這是一個須要更新的節點。
List節點處理完成,react仍然會檢查當前時間片是否夠用。若是夠用則處理下一個,也就是button。加入這個時候,用戶點擊了放大字體的按鈕。這個放大字體的操做,純粹由js實現,跟react無關。可是操做並不能當即生效,由於react的時間片還未用完,所以接下來仍然要繼續處理button。
button沒有任何子節點,因此此時能夠返回,並標誌button處理完成。若是button有改變,須要打上tag,可是當前狀況沒有,只須要標記完成便可。
老規矩,處理完一個節點先看時間夠不夠用。注意這裏放大字體的操做已經在等候釋放主線程了。
接下來處理第一個item。經過shouldComponentUpdate鉤子能夠根據傳入的props判斷其是否須要改變。對於第一個Item而言,更改先後都是1,因此不會改變,shouldComponentUpdate返回false,複製div,處理完成,檢查時間,若是還有時間進入第二個Item。
第二個Item shouldComponentUpdate返回true,因此須要打上tag,標誌須要更新,複製div,調用render,講div中的內容從2更新爲4,由於div有更新,因此標記div。當前節點處理完成。
對於上面這種狀況,div已是葉子節點,且沒有任何兄弟節點,且其值已經更新,這時候,須要將此節點改變產生的effect合併到父節點中。此時react會維護一個列表,其中記錄全部產生effect的元素。
合併後,回到父節點Item,父節點標記完成。
下一個工做單元是Item,在進入Item以前,檢查時間。但這個時候時間用完了。此時react必須交換主線程,並告訴主線程之後要爲其分配時間以完成剩下的操做。
主線程接下來進行放大字體的操做。完成後執行react接下來的操做,跟上一個Item的處理流程幾乎同樣,處理完成後整個fiber-tree和workInProgress以下:
完成後,Item向List返回並merge effect,effect List如今以下所示:
此時List向根節點返回並merge effect,全部節點均可以標記完成了。此時react將workInProgress標記爲pendingCommit。意思是能夠進入commit階段了。
此時,要作的是仍是檢查時間夠不夠用,若是沒有時間,會等到時間再去提交修改到DOM。進入到階段2後,reacDOM會根據階段1計算出來的effect-list來更新DOM。
更新完DOM以後,workInProgress就徹底和DOM保持一致了,爲了讓當前的fiber-tree和DOM保持一直,react交換了current和workinProgress兩個指針。
事實上,react大部分時間都在維持兩個樹(Double-buffering)。這能夠縮減下次更新時,分配內存、垃圾清理的時間。commit完成後,執行componentDidMount函數。
經過將reconciliation過程,分解成小的工做單元的方式,可讓頁面對於瀏覽器事件的響應更加及時。可是另一個問題仍是沒有解決,就是若是當前在處理的react渲染耗時較長,仍然會阻塞後面的react渲染。這就是爲何fiber reconciler增長了優先級策略。
module.exports = { NoWork: 0, // No work is pending. SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects. AnimationPriority: 2, // Needs to complete before the next frame. HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive. LowPriority: 4, // Data fetching, or result from updating stores. OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible. }; 複製代碼
優先級策略的核心是,在reconciliation階段,低優先級的操做能夠被高優先級的操做打斷,並讓主線程執行高優先級的更新,以時用戶可感知的響應更快。值得注意的一點是,當主線程從新分配給低優先級的操做時,並不會從上次工做的狀態開始,而是重新開始。
這就可能會產生兩個問題:
對於某些狀況來講,phase1階段的生命週期函數可能會不止執行一次。好比說,當一個低優先級的componentWillUpdate執行以後,被高優先級的打斷,高優先級執行完以後,再回到低優先級的操做中來,componentWillUpdate可能會再執行一次。對於某些只指望執行一次,或者須要在兩個生命週期函數的操做中執行對稱操做的狀況而言,要考慮這種case,確保不會讓整個App crash掉。