App下載

一文看懂React17新特性——啟發(fā)式更新算法

猿友 2020-08-14 15:52:19 瀏覽數(shù) (6031)
反饋

三天前,React團(tuán)隊(duì)發(fā)布了React17的第一個(gè)RC版本,這個(gè)版本最大的特性就是“無(wú)新特性”。

那么,從v16v17這一年多時(shí)間React團(tuán)隊(duì)究竟在做什么?

遙想從v15v16,React團(tuán)隊(duì)花了兩年時(shí)間將源碼架構(gòu)中的Stack Reconciler重構(gòu)為Fiber Reconciler,事情一定沒(méi)有這么簡(jiǎn)單。

事實(shí)上,這次版本更迭確實(shí)有“新特性” —— 替換了內(nèi)部使用的啟發(fā)式更新算法。

只不過(guò)這個(gè)特性對(duì)開(kāi)發(fā)者是無(wú)感知的。

本文接下來(lái)將講述如下內(nèi)容:

  • 起源:為什么會(huì)出現(xiàn)啟發(fā)式更新算法?
  • 現(xiàn)狀:React16的啟發(fā)式更新算法及他的不足
  • 未來(lái):React17的啟發(fā)式更新算法

為什么會(huì)出現(xiàn)啟發(fā)式更新算法

框架的運(yùn)行性能是框架設(shè)計(jì)者在設(shè)計(jì)框架時(shí)需要重點(diǎn)關(guān)注的點(diǎn)。

Vue使用模版語(yǔ)法,可以在編譯時(shí)對(duì)確定的模版作出優(yōu)化。

ReactJS寫(xiě)法太過(guò)靈活,使他在編譯時(shí)優(yōu)化方面先天不足。

所以,React的優(yōu)化主要在運(yùn)行時(shí)。

React15的痛點(diǎn)

在運(yùn)行時(shí)優(yōu)化方面,React一直在努力。

比如,React15實(shí)現(xiàn)了batchedUpdates(批量更新)。

即同一事件回調(diào)函數(shù)上下文中的多次setState只會(huì)觸發(fā)一次更新。

但是,如果單次更新就很耗時(shí),頁(yè)面還是會(huì)卡頓(這在一個(gè)維護(hù)時(shí)間很長(zhǎng)的大應(yīng)用中是很常見(jiàn)的)。

這是因?yàn)?code>React15的更新流程是同步執(zhí)行的,一旦開(kāi)始更新直到頁(yè)面渲染前都不能中斷。

為了解決同步更新長(zhǎng)時(shí)間占用線(xiàn)程導(dǎo)致頁(yè)面卡頓的問(wèn)題,也為了探索運(yùn)行時(shí)優(yōu)化的更多可能,React開(kāi)始重構(gòu)并一直持續(xù)至今。

重構(gòu)的目標(biāo)是實(shí)現(xiàn)Concurrent Mode(并發(fā)模式)。

(推薦教程:React教程

Concurrent Mode

Concurrent Mode的目的是實(shí)現(xiàn)一套可中斷/恢復(fù)的更新機(jī)制。

其由兩部分組成:

  • 一套協(xié)程架構(gòu)
  • 基于協(xié)程架構(gòu)的啟發(fā)式更新算法

其中,協(xié)程架構(gòu)就是React16中實(shí)現(xiàn)的Fiber Reconciler

我們可以將Fiber Reconciler理解為React自己實(shí)現(xiàn)的Generator。

Fiber Reconciler從理念到源碼的詳細(xì)介紹見(jiàn)這里

協(xié)程架構(gòu)使更新可以在需要的時(shí)機(jī)被中斷,這樣瀏覽器就有時(shí)間完成樣式布局與樣式繪制,減少卡頓(掉幀)的出現(xiàn)。

當(dāng)瀏覽器進(jìn)入下一次事件循環(huán),協(xié)程架構(gòu)可以恢復(fù)中斷或者拋棄之前的更新,重新開(kāi)始新的更新流程。

啟發(fā)式更新算法就是控制協(xié)程架構(gòu)工作方式的算法。

React16的啟發(fā)式更新算法

啟發(fā)式更新算法的啟發(fā)式指什么呢?

啟發(fā)式指不通過(guò)顯式的指派,而是通過(guò)優(yōu)先級(jí)調(diào)度更新。

其中優(yōu)先級(jí)來(lái)源于人機(jī)交互的研究成果。

比如:

人機(jī)交互的研究成果表明:

  • 當(dāng)用戶(hù)在輸入框輸入內(nèi)容時(shí),希望輸入的內(nèi)容能實(shí)時(shí)響應(yīng)在輸入框
  • 當(dāng)異步請(qǐng)求數(shù)據(jù)后,即使等待一會(huì)兒再顯示內(nèi)容,用戶(hù)也是可以接受的

基于此,在React16中

輸入框輸入內(nèi)容觸發(fā)的更新優(yōu)先級(jí) > 請(qǐng)求數(shù)據(jù)返回后觸發(fā)更新優(yōu)先級(jí)

算法實(shí)現(xiàn) 在React16、17中,在組件內(nèi)執(zhí)行this.setState后會(huì)在該組件對(duì)應(yīng)的fiber節(jié)點(diǎn)內(nèi)產(chǎn)生一種鏈表數(shù)據(jù)結(jié)構(gòu)update。

其中,update.expirationTimes為類(lèi)似時(shí)間戳的字段,表示優(yōu)先級(jí)。

expirationTimes從字面意義理解為過(guò)期時(shí)間。

該值離當(dāng)前時(shí)間越接近,該update 優(yōu)先級(jí)越高。

當(dāng)update.expirationTimes超過(guò)當(dāng)前時(shí)間,則代表該update過(guò)期,優(yōu)先級(jí)變?yōu)樽罡撸赐剑?/p>

一棵fiber樹(shù)的多個(gè)fiber節(jié)點(diǎn)可能存在多個(gè)update。

每次Fiber Reconciler調(diào)度更新時(shí),會(huì)在所有fiber節(jié)點(diǎn)的所有update.expirationTimes中選擇一個(gè)expirationTimes(一般選擇最大的),作為本次更新的優(yōu)先級(jí)。

并從根fiber節(jié)點(diǎn)開(kāi)始向下構(gòu)建新的fiber樹(shù)。

構(gòu)建過(guò)程中如果某個(gè)fiber節(jié)點(diǎn)包含update,且

update.expirationTimes >= expirationTimes

則該update對(duì)應(yīng)的state變化會(huì)體現(xiàn)在本次更新中。

可以理解為:每次更新,都會(huì)選定一個(gè)優(yōu)先級(jí)(expirationTimes),最終頁(yè)面會(huì)渲染為該優(yōu)先級(jí)對(duì)應(yīng)update的快照。

舉個(gè)例子,我們有如圖所示fiber樹(shù),當(dāng)前還沒(méi)有更新產(chǎn)生,所以沒(méi)有構(gòu)建中的fiber樹(shù)。

沒(méi)更新產(chǎn)生

當(dāng)在 C 創(chuàng)建一個(gè)低優(yōu)先級(jí)update,調(diào)度更新,本次更新選擇的優(yōu)先級(jí)為低優(yōu)先級(jí)。

開(kāi)始構(gòu)建新的fiber樹(shù)(圖右側(cè))。

構(gòu)建新的fiber樹(shù)

此時(shí),我們?cè)?D 創(chuàng)建一個(gè)高優(yōu)先級(jí)update。

這會(huì)中斷進(jìn)行中的低優(yōu)先級(jí)更新,重新開(kāi)始以高優(yōu)先級(jí)生成一棵fiber樹(shù)。

由于之前的更新被中斷,還沒(méi)有任何渲染操作,此時(shí)視圖中(左圖)還沒(méi)有任何變化。

優(yōu)先級(jí)update

本次更新選定的優(yōu)先級(jí)為高優(yōu)先級(jí),C 的update(低優(yōu)先級(jí))會(huì)被跳過(guò)。

更新完成后新的fiber樹(shù)會(huì)被渲染到視圖中。

新的fiber樹(shù)

由于 C 被跳過(guò),所以不會(huì)在視圖(左圖)中體現(xiàn)。

接下來(lái)我們?cè)?E 觸發(fā)一次高優(yōu)先級(jí)update

C 雖然包含低優(yōu)先級(jí)update,但隨著時(shí)間的推移,他的expirationTimes已經(jīng)過(guò)期,變?yōu)楦邇?yōu)先級(jí)。

C變?yōu)楦邇?yōu)先級(jí)

所以本次更新會(huì)有 C E 兩個(gè)fiber節(jié)點(diǎn)產(chǎn)生變化。

最終完成更新后,視圖如下:

最終更新

算法缺陷

如果只考慮中斷/繼續(xù)這樣的 CPU 操作,以expirationTimes大小作為衡量?jī)?yōu)先級(jí)依據(jù)的模型可以很好工作。

但是expirationTimes模型不能滿(mǎn)足 IO 操作(Suspense)。

在該模型下,高優(yōu)先級(jí) IO 任務(wù)(Suspense)會(huì)中斷低優(yōu)先級(jí) CPU 任務(wù)。

還記得么,每次更新,都是以某一優(yōu)先級(jí)作為整棵樹(shù)的優(yōu)先級(jí)更新標(biāo)準(zhǔn),而不僅僅是某一組件,即使更新的源頭(update)確實(shí)是某個(gè)組件產(chǎn)生的。

expirationTimes模型只能區(qū)分是否>=expirationTimes這種情況。

為了拓展Concurrent Mode能力邊界,需要一種更細(xì)粒度的啟發(fā)式優(yōu)先級(jí)更新算法。

(推薦教程:React入門(mén)實(shí)例教程

React17啟發(fā)式更新算法

最理想的模型是:可以指定任意幾個(gè)優(yōu)先級(jí),更新會(huì)以這些優(yōu)先級(jí)對(duì)應(yīng)update生成頁(yè)面快照。

但是現(xiàn)有架構(gòu)下,該方案實(shí)現(xiàn)上有瓶頸。

妥協(xié)之下,React17的解決方案是:指定一個(gè)連續(xù)的優(yōu)先級(jí)區(qū)間,每次更新都會(huì)以區(qū)間內(nèi)包含的優(yōu)先級(jí)生成對(duì)應(yīng)頁(yè)面快照。

這種優(yōu)先級(jí)區(qū)間模型被稱(chēng)為lanes(車(chē)道模型)。

具體做法是:使用一個(gè)31位的二進(jìn)制代表31種可能性。

  • 其中每個(gè)bit被稱(chēng)為一個(gè)lane(車(chē)道),代表優(yōu)先級(jí)
  • 某幾個(gè)lane組成的二進(jìn)制數(shù)被稱(chēng)為一個(gè)lanes,代表一批優(yōu)先級(jí)

可以從源碼中看到,從藍(lán)線(xiàn)一路劃下去,每個(gè)bit都對(duì)應(yīng)一個(gè)lanelanes。

對(duì)應(yīng)lanes

當(dāng)update產(chǎn)生,會(huì)根據(jù)React16同樣的啟發(fā)式方式,獲得如下優(yōu)先級(jí)的一種:

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)先級(jí)越大。

比如:

  • 點(diǎn)擊事件回調(diào)中觸發(fā)this.setState產(chǎn)生的update會(huì)獲得InputDiscreteLanePriority。
  • 同步的update會(huì)獲得SyncLanePriority。

接下來(lái),update會(huì)以priority為線(xiàn)索尋找沒(méi)被占用的lane。

如果當(dāng)前fiber樹(shù)已經(jīng)存在更新且更新的lanes包含了該lane,則update需要尋找其他lane。

比如,InputDiscreteLanePriority對(duì)應(yīng)的lanesInputDiscreteLanes。

// 第4、5位為1 const InputDiscreteLanes: Lanes = 0b0000000000000000000000000011000;

lanes包含第4、5位 2 個(gè) bit位。

如果其中

// 第五位為1 0b0000000000000000000000000010000

第五位的lane已經(jīng)被占用,則該update可以嘗試占有后一個(gè),即

// 第四位為1 0b0000000000000000000000000001000

如果InputDiscreteLanes的兩個(gè)lane都被占用,則該update的優(yōu)先級(jí)會(huì)下降到InputContinuousLanePriority并繼續(xù)尋找空余的lane

這個(gè)過(guò)程就像:購(gòu)物中心每一層(不同優(yōu)先級(jí))都有一個(gè)露天停車(chē)場(chǎng)(lanes),停車(chē)場(chǎng)有多個(gè)車(chē)位(lane)。

我們先開(kāi)車(chē)到頂樓找車(chē)位(lane),如果沒(méi)有車(chē)位就下一樓繼續(xù)找。

直到找到空余車(chē)位。

由于lanes可以包含多個(gè)lane,可以很方便的區(qū)分 IO 操作(Suspense)與 CPU 操作。

當(dāng)構(gòu)建fiber樹(shù)進(jìn)入構(gòu)建Suspense子樹(shù)時(shí),會(huì)將Suspenselane插入本次更新選定的lanes中。

當(dāng)構(gòu)建離開(kāi)Suspense子樹(shù)時(shí),會(huì)將Suspense lane從本次更新的lanes中移除。

(推薦微課:React微課

總結(jié)

React16expirationTimes模型只能區(qū)分是否>=expirationTimes決定節(jié)點(diǎn)是否更新。

React17lanes模型可以選定一個(gè)更新區(qū)間,并且動(dòng)態(tài)的向區(qū)間中增減優(yōu)先級(jí),可以處理更細(xì)粒度的更新。

以上就是關(guān)于React17的新特性--啟發(fā)式更新算法的相關(guān)介紹了,希望對(duì)大家有所幫助。

0 人點(diǎn)贊