給女朋友講React18新特性:startTransition
大家好,我是卡頌。
我女友是個(gè)鐵憨憨,又菜又愛(ài)玩。
她問(wèn)我:“卡卡,你說(shuō)時(shí)光真的可以重來(lái)?命運(yùn)真是可以選擇的么?”
我:“可以的,React18的新特性startTransition就行。”
startTransition的出現(xiàn)當(dāng)然不是為了逆轉(zhuǎn)命運(yùn),而是為了逆轉(zhuǎn)React的更新流程。
"在聊startTransition的具體應(yīng)用場(chǎng)景前,我先來(lái)聊聊React是如何揚(yáng)長(zhǎng)避短的。"
編譯時(shí)的短,運(yùn)行時(shí)的長(zhǎng)
如果我們用「重編譯時(shí)還是運(yùn)行時(shí)」區(qū)分前端框架。那么Vue和Svelte就是「重編譯時(shí)」的杰出代表。
在「編譯時(shí)」,這兩個(gè)框架可以分離模版語(yǔ)法中「變」與「不變」的部分,減少運(yùn)行時(shí)的代碼邏輯。
而React由于使用JSX(而非模版語(yǔ)法)描述視圖,走的是「重運(yùn)行時(shí)」的路線。
- 不是React不想在「編譯時(shí)」做優(yōu)化,奈何JSX實(shí)在太靈活,做不到啊......
所以他的優(yōu)化策略也都是偏「運(yùn)行時(shí)」。
在「運(yùn)行時(shí)」,最大的開(kāi)銷(xiāo)是:狀態(tài)更新到視圖變化中間的計(jì)算步驟。
這個(gè)步驟是通過(guò)「遍歷Fiber樹(shù)」實(shí)現(xiàn)的。
常規(guī)的「運(yùn)行時(shí)優(yōu)化策略」,比如:
- React.memo
- PureComponent
- shouldComponentUpdate
優(yōu)化方向都是:減少遍歷時(shí)需要遍歷的Fiber節(jié)點(diǎn)數(shù)量。
雖說(shuō)性能優(yōu)化的收益可以積少成多,但是React團(tuán)隊(duì)早已不滿(mǎn)足這種局部的小優(yōu)化。
性能優(yōu)化新思
路他們的思路是:
不同更新觸發(fā)的視圖變化顯然是有輕重緩急的。
如果能區(qū)分更新的優(yōu)先級(jí),讓高優(yōu)更新對(duì)應(yīng)的視圖變化先渲染,那么就能在設(shè)備性能不變的情況下,讓用戶(hù)更快看到他們想看到的UI。
比如:對(duì)于這樣一個(gè)搜索下拉框:
用戶(hù)期望:輸入框輸入的內(nèi)容要實(shí)時(shí)反映在視圖上(表現(xiàn)為輸入內(nèi)容不能卡頓)。
而結(jié)果下拉框的展示是可以有延遲的。
基于以上邏輯,React希望提供一個(gè)API,讓用戶(hù)告訴自己,哪些更新是「高優(yōu)」的,哪些是「低優(yōu)」的。
這樣,React就能知道優(yōu)先渲染誰(shuí)了。
這個(gè)API,就是startTransition。
startTransition的使用
接下來(lái),我們用一個(gè)Demo[1]演示startTransition的使用。
這個(gè)Demo會(huì)渲染一棵「畢達(dá)哥拉斯樹(shù)」。
拖動(dòng)左邊滑塊會(huì)改變樹(shù)渲染的節(jié)點(diǎn)數(shù)量。
拖動(dòng)頂部滑塊會(huì)改變樹(shù)的傾斜角度。
最頂上有個(gè)幀雷達(dá),可以實(shí)時(shí)顯示更新過(guò)程中的掉幀情況。
當(dāng)不點(diǎn)擊Use startTransition按鈕,拖動(dòng)頂上的滑塊。

可以看到:拖動(dòng)并不流暢,頂上的幀雷達(dá)顯示掉幀(出現(xiàn)黃色、紅色扇面)
當(dāng)點(diǎn)擊Use startTransition按鈕,拖動(dòng)頂上的滑塊。

可以明顯看到:拖動(dòng)變流暢,頂上的幀雷達(dá)顯示掉幀的情況變少
讓我們節(jié)選Demo的代碼看看,究竟發(fā)生了什么。
Demo都做了什么?
首先,控制滑塊、樹(shù)傾斜角度、要渲染的節(jié)點(diǎn)數(shù)量是分離在不同state中的:
- // 左側(cè)滑塊的state
- const [treeSizeInput, setTreeSizeInput] = useState(8);
- // 控制渲染節(jié)點(diǎn)數(shù)量的state
- const [treeSize, setTreeSize] = useState(8);
- // 頂部滑塊的state
- const [treeLeanInput, setTreeLeanInput] = useState(0);
- // 控制樹(shù)傾斜角度的state
- const [treeLean, setTreeLean] = useState(0);
- // startTransition的hook版本
- const [isLeaning, startTransition] = useTransition();
當(dāng)拖動(dòng)頂上的滑塊(改變樹(shù)的傾斜角度)會(huì)調(diào)用changeTreeLean方法:
- function changeTreeLean(event) {
- const value = Number(event.target.value);
- setTreeLeanInput(value); // update input
- // update visuals
- if (enableStartTransition) {
- startTransition(() => {
- setTreeLean(value);
- });
- } else {
- setTreeLean(value);
- }
- }
該方法會(huì)改變兩個(gè)state:
- 通過(guò)調(diào)用setTreeLeanInput改變頂部滑塊位置相關(guān)的state —— treeLeanInput
- 通過(guò)調(diào)用setTreeLean改變樹(shù)的傾斜角度相關(guān)的state —— treeLean
是否點(diǎn)擊Use startTransition按鈕的區(qū)別,就在于setTreeLean是否會(huì)被作為startTransition的回調(diào)執(zhí)行:
- // 是否開(kāi)啟startTransition
- if (enableStartTransition) {
- startTransition(() => {
- setTreeLean(value);
- });
- } else {
- setTreeLean(value);
- }
當(dāng)作為startTransition的回調(diào)執(zhí)行時(shí),setTreeLean改變的狀態(tài)(treeLean)對(duì)應(yīng)的視圖變化(即:改變樹(shù)的傾斜角度)會(huì)被視為「低優(yōu)先級(jí)的更新」。
即使其與改變滑塊狀態(tài)的方法(setTreeLeanInput)在同一上下文中執(zhí)行,
由于其優(yōu)先級(jí)較低,React會(huì)優(yōu)先處理「改變滑塊狀態(tài)」對(duì)應(yīng)的視圖變化。
表現(xiàn)為:滑塊的滑動(dòng)不卡頓。
startTransition的原理
鐵憨憨:“這么酷炫的功能實(shí)現(xiàn)起來(lái)一定很復(fù)雜吧?”
“恰恰相反,依賴(lài)于React底層實(shí)現(xiàn)的優(yōu)先級(jí)調(diào)度模型,startTransition的實(shí)現(xiàn)其實(shí)很簡(jiǎn)單!”
以剛才的代碼為例,如果加上console.log打?。?/p>
- console.log(1);
- startTransition(() => {
- console.log(2);
- setTreeLean(value);
- });
- console.log(3);
那么會(huì)依次輸出:123
startTransition做的事情很簡(jiǎn)單,類(lèi)似這樣:
- let isInTransition = false
- function startTransition(fn) {
- isInTransition = true
- fn()
- isInTransition = false
- }
也就是說(shuō),當(dāng)調(diào)用startTransition,在其上下文中獲取到的全局變量isInTransition為true。
如果startTransition的回調(diào)函數(shù)fn中包含更新?tīng)顟B(tài)的方法(比如上文Demo中的setTreeLean),
那么這次更新就會(huì)被標(biāo)記為isTransition,類(lèi)似這樣:
- // 調(diào)用setTreeLean后會(huì)執(zhí)行的方法(偽代碼)
- function setState(value) {
- stateQueue.push({
- nextState: value,
- isTransition: isInTransition
- })
- }
代表這是一個(gè)低優(yōu)先級(jí)的過(guò)渡更新。
接下來(lái),就是React內(nèi)部的調(diào)度、批處理與更新流程了。
- 批處理的邏輯見(jiàn)給女朋友講React18新特性:Automatic batching
總結(jié)
今天,我們講了:
- React為了彌補(bǔ)自身弱編譯時(shí)的缺點(diǎn),在運(yùn)行時(shí)作出的努力
- startTransition本質(zhì)是讓開(kāi)發(fā)者手動(dòng)標(biāo)記更新的優(yōu)先級(jí)
- startTransition的實(shí)現(xiàn)原理
鐵憨憨:”原來(lái)React為了性能優(yōu)化做了這么多努力,好復(fù)雜啊,我還是用Vue吧!“
我:“可不是嘛,React已經(jīng)在朝著實(shí)現(xiàn)一個(gè)瀏覽器的方向發(fā)展了。”
參考資料
[1]Demo:
https://swizec.com/blog/a-better-react-18-starttransition-demo/