三天前,React
團隊發(fā)布了React17
的第一個RC
版本,這個版本最大的特性就是“無新特性”。
那么,從v16
到v17
這一年多時間React
團隊究竟在做什么?
遙想從v15
到v16
,React
團隊花了兩年時間將源碼架構中的Stack Reconciler
重構為Fiber Reconciler
,事情一定沒有這么簡單。
事實上,這次版本更迭確實有“新特性” —— 替換了內部使用的啟發(fā)式更新算法。
只不過這個特性對開發(fā)者是無感知的。
本文接下來將講述如下內容:
- 起源:為什么會出現啟發(fā)式更新算法?
- 現狀:React16的啟發(fā)式更新算法及他的不足
- 未來:React17的啟發(fā)式更新算法
為什么會出現啟發(fā)式更新算法
框架的運行性能是框架設計者在設計框架時需要重點關注的點。
Vue
使用模版語法,可以在編譯時對確定的模版作出優(yōu)化。
而React
純JS
寫法太過靈活,使他在編譯時優(yōu)化方面先天不足。
所以,React
的優(yōu)化主要在運行時。
React15的痛點
在運行時優(yōu)化方面,React
一直在努力。
比如,React15
實現了batchedUpdates
(批量更新)。
即同一事件回調函數上下文中的多次setState
只會觸發(fā)一次更新。
但是,如果單次更新就很耗時,頁面還是會卡頓(這在一個維護時間很長的大應用中是很常見的)。
這是因為React15
的更新流程是同步執(zhí)行的,一旦開始更新直到頁面渲染前都不能中斷。
為了解決同步更新長時間占用線程導致頁面卡頓的問題,也為了探索運行時優(yōu)化的更多可能,React
開始重構并一直持續(xù)至今。
重構的目標是實現Concurrent Mode
(并發(fā)模式)。
(推薦教程:React教程)
Concurrent Mode
Concurrent Mode
的目的是實現一套可中斷/恢復的更新機制。
其由兩部分組成:
- 一套協程架構
- 基于協程架構的啟發(fā)式更新算法
其中,協程架構就是React16
中實現的Fiber Reconciler
。
我們可以將Fiber Reconciler
理解為React
自己實現的Generator
。
Fiber Reconciler從理念到源碼的詳細介紹見這里
協程架構使更新可以在需要的時機被中斷,這樣瀏覽器就有時間完成樣式布局與樣式繪制,減少卡頓(掉幀)的出現。
當瀏覽器進入下一次事件循環(huán),協程架構可以恢復中斷或者拋棄之前的更新,重新開始新的更新流程。
啟發(fā)式更新算法就是控制協程架構工作方式的算法。
React16的啟發(fā)式更新算法
啟發(fā)式更新算法的啟發(fā)式指什么呢?
啟發(fā)式指不通過顯式的指派,而是通過優(yōu)先級調度更新。
其中優(yōu)先級來源于人機交互的研究成果。
比如:
人機交互的研究成果表明:
- 當用戶在輸入框輸入內容時,希望輸入的內容能實時響應在輸入框
- 當異步請求數據后,即使等待一會兒再顯示內容,用戶也是可以接受的
基于此,在React16中
輸入框輸入內容觸發(fā)的更新
優(yōu)先級 > 請求數據返回后觸發(fā)更新
優(yōu)先級
算法實現
在React16、17
中,在組件內執(zhí)行this.setState
后會在該組件對應的fiber
節(jié)點內產生一種鏈表數據結構update
。
其中,update.expirationTimes
為類似時間戳的字段,表示優(yōu)先級。
expirationTimes
從字面意義理解為過期時間。
該值離當前時間越接近,該update
優(yōu)先級越高。
當update.expirationTimes
超過當前時間,則代表該update
過期,優(yōu)先級變?yōu)樽罡撸赐剑?/p>
一棵fiber
樹的多個fiber
節(jié)點可能存在多個update
。
每次Fiber Reconciler
調度更新時,會在所有fiber
節(jié)點的所有update.expirationTimes
中選擇一個expirationTimes
(一般選擇最大的),作為本次更新的優(yōu)先級。
并從根fiber
節(jié)點開始向下構建新的fiber
樹。
構建過程中如果某個fiber
節(jié)點包含update
,且
update.expirationTimes >= expirationTimes
則該update
對應的state
變化會體現在本次更新中。
可以理解為:每次更新,都會選定一個優(yōu)先級(expirationTimes),最終頁面會渲染為該優(yōu)先級對應update
的快照。
舉個例子,我們有如圖所示fiber
樹,當前還沒有更新產生,所以沒有構建中的fiber
樹。
當在 C 創(chuàng)建一個低優(yōu)先級update
,調度更新,本次更新選擇的優(yōu)先級為低優(yōu)先級。
開始構建新的fiber
樹(圖右側)。
此時,我們在 D 創(chuàng)建一個高優(yōu)先級update
。
這會中斷進行中的低優(yōu)先級更新,重新開始以高優(yōu)先級生成一棵fiber
樹。
由于之前的更新被中斷,還沒有任何渲染操作,此時視圖中(左圖)還沒有任何變化。
本次更新選定的優(yōu)先級為高優(yōu)先級,C 的update
(低優(yōu)先級)會被跳過。
更新完成后新的fiber
樹會被渲染到視圖中。
由于 C 被跳過,所以不會在視圖(左圖)中體現。
接下來我們在 E 觸發(fā)一次高優(yōu)先級update
。
C 雖然包含低優(yōu)先級update
,但隨著時間的推移,他的expirationTimes
已經過期,變?yōu)楦邇?yōu)先級。
所以本次更新會有 C E 兩個fiber
節(jié)點產生變化。
最終完成更新后,視圖如下:
算法缺陷
如果只考慮中斷/繼續(xù)這樣的 CPU 操作,以expirationTimes
大小作為衡量優(yōu)先級依據的模型可以很好工作。
但是expirationTimes
模型不能滿足 IO 操作(Suspense)。
在該模型下,高優(yōu)先級 IO 任務(Suspense)會中斷低優(yōu)先級 CPU 任務。
還記得么,每次更新,都是以某一優(yōu)先級作為整棵樹的優(yōu)先級更新標準,而不僅僅是某一組件,即使更新的源頭(update)確實是某個組件產生的。
expirationTimes
模型只能區(qū)分是否>=expirationTimes
這種情況。
為了拓展Concurrent Mode
能力邊界,需要一種更細粒度的啟發(fā)式優(yōu)先級更新算法。
(推薦教程:React入門實例教程)
React17啟發(fā)式更新算法
最理想的模型是:可以指定任意幾個優(yōu)先級,更新會以這些優(yōu)先級對應update
生成頁面快照。
但是現有架構下,該方案實現上有瓶頸。
妥協之下,React17
的解決方案是:指定一個連續(xù)的優(yōu)先級區(qū)間,每次更新都會以區(qū)間內包含的優(yōu)先級生成對應頁面快照。
這種優(yōu)先級區(qū)間模型被稱為lanes
(車道模型)。
具體做法是:使用一個31位的二進制代表31種可能性。
- 其中每個
bit
被稱為一個lane
(車道),代表優(yōu)先級 - 某幾個
lane
組成的二進制數被稱為一個lanes
,代表一批優(yōu)先級
可以從源碼中看到,從藍線一路劃下去,每個bit都對應一個lane
或lanes
。
當update
產生,會根據React16
同樣的啟發(fā)式方式,獲得如下優(yōu)先級的一種:
export const SyncLanePriority: LanePriority = 17; export const SyncBatchedLanePriority: LanePriority = 16; export const InputDiscreteLanePriority: LanePriority = 14; export const InputContinuousLanePriority: LanePriority = 12; export const DefaultLanePriority: LanePriority = 10; export const TransitionShortLanePriority: LanePriority = 8; export const TransitionLongLanePriority: LanePriority = 6;
其中值越高,優(yōu)先級越大。
比如:
- 點擊事件回調中觸發(fā)
this.setState
產生的update
會獲得InputDiscreteLanePriority
。 - 同步的
update
會獲得SyncLanePriority
。
接下來,update
會以priority
為線索尋找沒被占用的lane
。
如果當前fiber
樹已經存在更新且更新的lanes
包含了該lane
,則update
需要尋找其他lane
。
比如,InputDiscreteLanePriority
對應的lanes
為InputDiscreteLanes
。
// 第4、5位為1 const InputDiscreteLanes: Lanes = 0b0000000000000000000000000011000;
該lanes
包含第4、5位 2 個 bit
位。
如果其中
// 第五位為1 0b0000000000000000000000000010000
第五位的lane
已經被占用,則該update
可以嘗試占有后一個,即
// 第四位為1 0b0000000000000000000000000001000
如果InputDiscreteLanes
的兩個lane
都被占用,則該update
的優(yōu)先級會下降到InputContinuousLanePriority
并繼續(xù)尋找空余的lane
。
這個過程就像:購物中心每一層(不同優(yōu)先級)都有一個露天停車場(lanes),停車場有多個車位(lane)。
我們先開車到頂樓找車位(lane),如果沒有車位就下一樓繼續(xù)找。
直到找到空余車位。
由于lanes
可以包含多個lane
,可以很方便的區(qū)分 IO 操作(Suspense)與 CPU 操作。
當構建fiber
樹進入構建Suspense
子樹時,會將Suspense
的lane
插入本次更新選定的lanes
中。
當構建離開Suspense
子樹時,會將Suspense lane
從本次更新的lanes
中移除。
(推薦微課:React微課)
總結
React16
的expirationTimes
模型只能區(qū)分是否>=expirationTimes
決定節(jié)點是否更新。
React17
的lanes
模型可以選定一個更新區(qū)間,并且動態(tài)的向區(qū)間中增減優(yōu)先級,可以處理更細粒度的更新。
以上就是關于React17
的新特性--啟發(fā)式更新算法的相關介紹了,希望對大家有所幫助。