關(guān)于React的一切——React的并發(fā)悖論
大家好,我卡頌。
當(dāng)一個(gè)React應(yīng)用邏輯變得復(fù)雜后,「組件render」花費(fèi)的時(shí)間會(huì)顯著增長(zhǎng)。如果從「組件render」到「視圖渲染」期間消耗的時(shí)間過(guò)長(zhǎng),用戶就會(huì)感知到頁(yè)面卡頓。
為了解決這個(gè)問(wèn)題,有兩個(gè)方法:
- 讓「組件render」的過(guò)程從同步變?yōu)楫惒?,這樣render過(guò)程頁(yè)面不會(huì)卡死。這就是并發(fā)更新的原理。
- 減少需要render的組件數(shù)量,這就是常說(shuō)的React性能優(yōu)化。
通常,對(duì)于不同類(lèi)型組件,我們會(huì)采取以上不同的方法。比如,對(duì)于下面這樣的有耗時(shí)邏輯的輸入框,方法1更合適(因?yàn)椴l(fā)更新能減少輸入時(shí)的卡頓):
那么,能不能在整個(gè)應(yīng)用層面同時(shí)兼顧這2種方式呢?答案是 —— 不太行。
這是因?yàn)?,?duì)于復(fù)雜應(yīng)用,并發(fā)更新與性能優(yōu)化通常是相悖的。就是本文要聊的 —— 并發(fā)悖論。
從性能優(yōu)化聊起
對(duì)于一個(gè)組件,如果希望他非必要時(shí)不render,需要達(dá)到的基本條件是:props的引用不變。
比如,下面代碼中Child組件依賴fn props,由于fn是內(nèi)聯(lián)形式,所以每次App組件render時(shí)引用都會(huì)變,不利于Child性能優(yōu)化:
為了Child性能優(yōu)化,可以將fn抽離出來(lái):
當(dāng)fn依賴某些props或者state時(shí),我們需要使用useCallback:
類(lèi)似的,其他類(lèi)型變量需要用到useMemo。
也就是說(shuō),當(dāng)涉及到性能優(yōu)化時(shí),React的代碼邏輯會(huì)變得復(fù)雜(需要考慮引用變化問(wèn)題)。
當(dāng)應(yīng)用進(jìn)一步復(fù)雜,會(huì)面臨更多問(wèn)題,比如:
- 復(fù)雜的useEffect邏輯。
- 狀態(tài)如何共享。
這些問(wèn)題會(huì)與性能優(yōu)化問(wèn)題互相疊加,最終導(dǎo)致應(yīng)用不僅邏輯復(fù)雜,性能也欠佳。
性能優(yōu)化的解決之道
好在,這些問(wèn)題有個(gè)共同的解決方法 —— 狀態(tài)管理。
上文我們聊到,對(duì)于性能優(yōu)化,關(guān)鍵的問(wèn)題是 —— 保持props引用不變。
在原生React中,如果a依賴b,b依賴c。那么,當(dāng)a變化后,我們需要通過(guò)各種方法(比如useCallback、useMemo)保持b、c引用的穩(wěn)定。
做這件事情本身(保持引用不變)對(duì)開(kāi)發(fā)者來(lái)說(shuō)就是額外的心智負(fù)擔(dān)。那么,狀態(tài)管理是如何解決這個(gè)問(wèn)題的呢?
答案是:狀態(tài)管理庫(kù)自己管理所有原始狀態(tài)以及派生狀態(tài)。
比如:
- 在Recoil中,基礎(chǔ)狀態(tài)類(lèi)型被稱為Atom,其他派生狀態(tài)都是基于Atom組合而來(lái)
- 在Zustand中,基礎(chǔ)狀態(tài)都是create方法創(chuàng)建的實(shí)例
- 在Redux中,維護(hù)了一個(gè)全局狀態(tài),對(duì)于需要用到的狀態(tài)通過(guò)selector從中摘出來(lái)
這些狀態(tài)管理方案都會(huì)自己維護(hù)所有的基礎(chǔ)狀態(tài)與派生狀態(tài)。當(dāng)開(kāi)發(fā)者從狀態(tài)管理庫(kù)中引入狀態(tài)時(shí),就能最大限度保持props引用不變。
比如,下例用Zustand改造上面的代碼。由于狀態(tài)a和依賴a的fn都是由Zustand管理,所以fn的引用始終不變:
并發(fā)更新的問(wèn)題
現(xiàn)在我們知道,性能優(yōu)化的通用解決途徑是 —— 通過(guò)狀態(tài)管理庫(kù),維護(hù)一套邏輯自洽的外部狀態(tài)(這里的「外部」是區(qū)別于React自身的狀態(tài)),保持引用不變。
但是,這套外部狀態(tài)最終一定會(huì)轉(zhuǎn)化為React的內(nèi)部狀態(tài)(再通過(guò)內(nèi)部狀態(tài)的變化驅(qū)動(dòng)視圖更新),所以就存在狀態(tài)同步時(shí)機(jī)的問(wèn)題。即:什么時(shí)候?qū)⑼獠繝顟B(tài)與內(nèi)部狀態(tài)同步?
在并發(fā)更新之前的React中,這并不是個(gè)問(wèn)題。因?yàn)楦率峭?、不?huì)被打斷的。所以對(duì)于同一個(gè)外部狀態(tài),在整個(gè)更新過(guò)程中都能保持不變。
比如,在如下代碼中,由于List?組件的render?過(guò)程不會(huì)打斷,所以list在遍歷過(guò)程中是穩(wěn)定的:
但是,對(duì)于開(kāi)啟并發(fā)更新的React,更新流程可能中斷,不同的Item組件可能是在中斷前后不同的宏任務(wù)中render,傳遞給他們的data props可能并不相同。這就導(dǎo)致同一次更新,同一個(gè)狀態(tài)(例子中的list)前后不一致的情況。
這種情況被稱為tearing(視圖撕裂)。
可以發(fā)現(xiàn),造成tearing的原因是 —— 外部狀態(tài)(狀態(tài)管理庫(kù)維護(hù)的狀態(tài))與React內(nèi)部狀態(tài)的同步時(shí)機(jī)出問(wèn)題。
這個(gè)問(wèn)題在當(dāng)前React中是很難解決的。退而求其次,為了讓這些狀態(tài)庫(kù)能夠正常使用,React專(zhuān)門(mén)出了個(gè)hook —— useSyncExternalStore。用于將狀態(tài)管理庫(kù)觸發(fā)的更新都以同步的方式執(zhí)行,這樣就不會(huì)有同步時(shí)機(jī)的問(wèn)題。
既然是以同步的方式執(zhí)行,那肯定沒(méi)法并發(fā)更新啦~~~
總結(jié)
實(shí)際上,凡是涉及到「自己維護(hù)了一個(gè)外部狀態(tài)」的庫(kù)(比如動(dòng)畫(huà)庫(kù)),都涉及到狀態(tài)同步的問(wèn)題,很有可能無(wú)法兼容并發(fā)更新。
所以,你會(huì)更傾向下面哪種選擇呢:
- 不care并發(fā)更新,以前React怎么用,現(xiàn)在就怎么用。
- 根據(jù)項(xiàng)目情況,平衡并發(fā)更新與性能優(yōu)化的訴求。