你需要知道關(guān)于React-18的幾個(gè)新功能

來源 | https://github.com/reactwg/react-18/discussions
整理 | 楊小二
1、自動批處理以減少渲染
什么是批處理?
批處理是 React將多個(gè)狀態(tài)更新分組到單個(gè)重新渲染中以獲得更好的性能。
例如,如果你在同一個(gè)點(diǎn)擊事件中有兩個(gè)狀態(tài)更新,React 總是將它們分批處理到一個(gè)重新渲染中。如果你運(yùn)行下面的代碼,你會看到每次點(diǎn)擊時(shí),React 只執(zhí)行一次渲染,盡管你設(shè)置了兩次狀態(tài):
function App () {const [ count , setCount ] = useState ( 0 ) ;const [ flag , setFlag ] = useState ( false ) ;function handleClick ( ) {setCount ( c => c + 1 ) ; // 還沒有重新渲染setFlag ( f => ! f ) ; // 還沒有重新渲染// React 只會在最后重新渲染一次(這是批處理!)}return (< div >< button onClick = { handleClick } > Next < / button >< h1 style = { { color : flag ? "blue" : "black" } } > { count } < / h1 >< / div >) ;}
這對性能非常有用,因?yàn)樗苊饬瞬槐匾闹匦落秩尽K€可以防止你的組件呈現(xiàn)僅更新一個(gè)狀態(tài)變量的“半完成”狀態(tài),這可能會導(dǎo)致錯(cuò)誤。
這可能會讓你想起餐廳服務(wù)員在你選擇第一道菜時(shí)不會跑到廚房,而是等你完成訂單。
然而,React 的批量更新時(shí)間并不一致。例如,如果你需要獲取數(shù)據(jù),然后更新handleClick上面的狀態(tài),那么 React不會批量更新,而是執(zhí)行兩次獨(dú)立的更新。
這是因?yàn)?React 過去只在瀏覽器事件(如點(diǎn)擊)期間批量更新,但這里我們在事件已經(jīng)被處理(在 fetch 回調(diào)中)之后更新狀態(tài):
function App() {const [count, setCount] = useState(0);const [flag, setFlag] = useState(false);function handleClick() {fetchSomething().then(() => {// React 17 及更早版本不會對這些進(jìn)行批處理,因?yàn)?/span>// 它們在回調(diào)中 *after* 事件運(yùn)行,而不是 *during* 它setCount ( c => c + 1 ) ; // 導(dǎo)致重新渲染setFlag ( f => ! f ) ; // 導(dǎo)致重新渲染} );}return (<div><button onClick={handleClick}>Next</button><h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1></div>);}
在 React 18 之前,我們只在 React 事件處理程序期間批量更新。默認(rèn)情況下,React 中不會對 promise、setTimeout、本機(jī)事件處理程序或任何其他事件中的更新進(jìn)行批處理。
什么是自動批處理?
從 React 18 開始createRoot,所有更新都將自動批處理,無論它們來自何處。
這意味著超時(shí)、承諾、本機(jī)事件處理程序或任何其他事件內(nèi)的更新將以與 React 事件內(nèi)的更新相同的方式進(jìn)行批處理。
我們希望這會導(dǎo)致更少的渲染工作,從而在你的應(yīng)用程序中獲得更好的性能:
function App() {const [count, setCount] = useState(0);const [flag, setFlag] = useState(false);function handleClick() {fetchSomething().then(() => {// React 18 及更高版本確實(shí)批處理這些:setCount ( c => c + 1 ) ;setFlag ( f => ! f ) ;// React 只會在最后重新渲染一次(這是批處理!)});}return (<div><button onClick={handleClick}>Next</button><h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1></div>);}
注意:作為采用 React 18 的一部分,預(yù)計(jì)你將升級到createRoot。舊行為的render存在只是為了更容易地對兩個(gè)版本進(jìn)行生產(chǎn)實(shí)驗(yàn)。
無論更新發(fā)生在何處,React 都會自動批量更新,因此:
function handleClick() {setCount(c => c + 1);setFlag(f => !f);// React will only re-render once at the end (that's batching!)}
行為與此相同:
setTimeout(() => {setCount(c => c + 1);setFlag(f => !f);// React will only re-render once at the end (that's batching!)}, 1000);
行為與此相同:
fetch(/*...*/).then(() => {setCount(c => c + 1);setFlag(f => !f);// React will only re-render once at the end (that's batching!)})
行為與此相同:
elm.addEventListener('click', () => {setCount(c => c + 1);setFlag(f => !f);// React will only re-render once at the end (that's batching!)});
注意:React 僅在通常安全的情況下才批量更新。
例如,React 確保對于每個(gè)用戶啟動的事件(如單擊或按鍵),DOM 在下一個(gè)事件之前完全更新。例如,這可確保在提交時(shí)禁用的表單不能被提交兩次。
如果我不想批處理怎么辦?
import { flushSync } from 'react-dom'; // Note: react-dom, not reactfunction handleClick() {flushSync(() => {setCounter(c => c + 1);});// React has updated the DOM by nowflushSync(() => {setFlag(f => !f);});// React has updated the DOM by now}
2、Suspense 的 SSR 支持
這基本上是服務(wù)器端渲染 (SSR) 邏輯的擴(kuò)展。在典型的 React SSR 應(yīng)用程序中,會發(fā)生以下步驟:
服務(wù)器獲取需要在 UI 上顯示的相關(guān)數(shù)據(jù)
服務(wù)器將整個(gè)應(yīng)用程序呈現(xiàn)為 HTML 并將其發(fā)送給客戶端作為響應(yīng)
客戶端下載 JavaScript 包(除了 HTML)
在最后一步,客戶端將 javascript 邏輯連接到 HTML(稱為 hydration)
典型 SSR 應(yīng)用程序的問題在于,在下一步可以開始之前,必須立即完成整個(gè)應(yīng)用程序的每個(gè)步驟。這會使您的應(yīng)用程序在初始加載時(shí)變慢且無響應(yīng)。
React 18 正試圖解決這個(gè)問題。<Suspense> 組件已經(jīng)以這樣的方式進(jìn)行了革命性的改變,它將應(yīng)用程序分解為更小的獨(dú)立單元,這些單元經(jīng)過提到的每個(gè)步驟。這樣一旦用戶看到內(nèi)容,它就會變成互動的。
3、startTransition
什么是過渡?
我們將狀態(tài)更新分為兩類:
緊急更新反映直接交互,如打字、懸停、拖動等。
過渡更新將 UI 從一個(gè)視圖過渡到另一個(gè)視圖。
單擊、懸停、滾動或打字等緊急更新需要立即響應(yīng)以匹配我們對物理對象行為方式的直覺。否則他們會覺得“錯(cuò)了”。
然而,轉(zhuǎn)換是不同的,因?yàn)橛脩舨幌M谄聊簧峡吹矫總€(gè)中間值。
例如,當(dāng)您在下拉列表中選擇過濾器時(shí),您希望過濾器按鈕本身在您單擊時(shí)立即響應(yīng)。但是,實(shí)際結(jié)果可能會單獨(dú)轉(zhuǎn)換。
一個(gè)小的延遲是難以察覺的,而且通常是預(yù)料之中的。如果在結(jié)果渲染完成之前再次更改過濾器,您只關(guān)心看到最新的結(jié)果。
在典型的 React 應(yīng)用程序中,大多數(shù)更新在概念上都是過渡更新。但出于向后兼容性的原因,過渡是可選的。
默認(rèn)情況下,React 18 仍然將更新處理為緊急更新,您可以通過將更新包裝到startTransition.
這解決了什么問題?
構(gòu)建流暢且響應(yīng)迅速的應(yīng)用程序并不總是那么容易。有時(shí),諸如單擊按鈕或輸入輸入之類的小動作可能會導(dǎo)致屏幕上發(fā)生很多事情。這可能會導(dǎo)致頁面在所有工作完成時(shí)凍結(jié)或掛起。
例如,考慮在過濾數(shù)據(jù)列表的輸入字段中鍵入。您需要將字段的值存儲在 state 中,以便您可以過濾數(shù)據(jù)并控制該輸入字段的值。您的代碼可能如下所示:
// 更新輸入值和搜索結(jié)果setSearchQuery ( input ) ;
在這里,每當(dāng)用戶鍵入一個(gè)字符時(shí),我們都會更新輸入值并使用新值來搜索列表并顯示結(jié)果。
對于大屏幕更新,這可能會導(dǎo)致頁面在呈現(xiàn)所有內(nèi)容時(shí)出現(xiàn)延遲,從而使打字或其他交互感覺緩慢且無響應(yīng)。
即使列表不是太長,列表項(xiàng)本身也可能很復(fù)雜并且每次擊鍵時(shí)都不同,并且可能沒有明確的方法來優(yōu)化它們的呈現(xiàn)。
從概念上講,問題在于需要進(jìn)行兩種不同的更新。第一個(gè)更新是緊急更新,用于更改輸入字段的值,以及可能會更改其周圍的一些 UI。
第二個(gè)是顯示搜索結(jié)果的不太緊急的更新。
// 緊急:顯示輸入的內(nèi)容setInputValue ( input ) ;// 不急:顯示結(jié)果setSearchQuery ( input ) ;
用戶希望第一次更新是即時(shí)的,因?yàn)檫@些交互的本機(jī)瀏覽器處理速度很快。但是第二次更新可能會有點(diǎn)延遲。
用戶不希望它立即完成,這很好,因?yàn)榭赡苡泻芏喙ぷ饕觥#▽?shí)際上,開發(fā)人員經(jīng)常使用去抖動等技術(shù)人為地延遲此類更新。)
在 React 18 之前,所有更新都被緊急渲染。
這意味著上面的兩個(gè)狀態(tài)仍然會同時(shí)呈現(xiàn),并且仍然會阻止用戶看到他們交互的反饋,直到一切都呈現(xiàn)出來。我們?nèi)鄙俚氖且环N告訴 React 哪些更新是緊急的,哪些不是的方法。
新startTransitionAPI 通過讓您能夠?qū)⒏聵?biāo)記為“轉(zhuǎn)換”來解決此問題:
import { startTransition } from 'react' ;// 緊急:顯示輸入的內(nèi)容setInputValue ( input ) ;// 將內(nèi)部的任何狀態(tài)更新標(biāo)記為轉(zhuǎn)換startTransition ( ( ) => {// Transition: 顯示結(jié)果setSearchQuery ( input ) ;} ) ;
包裝在其中的更新startTransition被視為非緊急處理,如果出現(xiàn)更緊急的更新(如點(diǎn)擊或按鍵),則會中斷。
如果用戶中斷轉(zhuǎn)換(例如,連續(xù)輸入多個(gè)字符),React 將拋出未完成的陳舊渲染工作,僅渲染最新更新。
Transitions 可讓您保持大多數(shù)交互敏捷,即使它們導(dǎo)致顯著的 UI 更改。它們還可以讓您避免浪費(fèi)時(shí)間渲染不再相關(guān)的內(nèi)容。
它與 setTimeout 有何不同?
上述問題的一個(gè)常見解決方案是將第二次更新包裝在 setTimeout 中:
// 顯示你輸入的內(nèi)容setInputValue ( input ) ;// 顯示結(jié)果setTimeout ( ( ) => {setSearchQuery ( input ) ;} , 0 ) ;
這將延遲第二次更新,直到呈現(xiàn)第一次更新之后。節(jié)流和去抖動是這種技術(shù)的常見變體。
一個(gè)重要的區(qū)別是startTransition不安排在以后喜歡的setTimeout是。它立即執(zhí)行。傳遞給的函數(shù)startTransition同步運(yùn)行,但其中的任何更新都標(biāo)記為“轉(zhuǎn)換”。
React 將在稍后處理更新時(shí)使用此信息來決定如何呈現(xiàn)更新。這意味著我們比在超時(shí)中包裝更新更早地開始呈現(xiàn)更新。
在快速設(shè)備上,兩次更新之間的延遲非常小。在較慢的設(shè)備上,延遲會更大,但 UI 會保持響應(yīng)。
另一個(gè)重要的區(qū)別是 a 內(nèi)的大屏幕更新setTimeout仍然會鎖定頁面,只是在超時(shí)之后。
如果用戶在超時(shí)觸發(fā)時(shí)仍在鍵入或與頁面交互,他們?nèi)詫⒈蛔柚古c頁面交互。但是標(biāo)記為 的狀態(tài)更新startTransition是可中斷的,因此它們不會鎖定頁面。
它們讓瀏覽器在呈現(xiàn)不同組件之間的小間隙中處理事件。
如果用戶輸入發(fā)生變化,React 將不必繼續(xù)渲染用戶不再感興趣的內(nèi)容。
最后,因?yàn)閟etTimeout只是延遲更新,顯示加載指示器需要編寫異步代碼,這通常很脆弱。
通過轉(zhuǎn)換,React 可以為您跟蹤掛起狀態(tài),根據(jù)轉(zhuǎn)換的當(dāng)前狀態(tài)更新它,并讓您能夠在用戶等待時(shí)顯示加載反饋。
我可以在哪里使用它?
您可以使用startTransition來包裝要移動到后臺的任何更新。通常,這些類型的更新分為兩類:
緩慢渲染:這些更新需要時(shí)間,因?yàn)?React 需要執(zhí)行大量工作才能轉(zhuǎn)換 UI 以顯示結(jié)果。
慢速網(wǎng)絡(luò):這些更新需要時(shí)間,因?yàn)?React 正在等待來自網(wǎng)絡(luò)的一些數(shù)據(jù)。此用例與 Suspense 緊密集成。
總結(jié)
React 18 沒有任何重大更改,因此,我們將當(dāng)前的存儲庫升級到最新版本幾乎不需要更改代碼,但我們可以享受它們很酷的功能。
最后,感謝你的閱讀。
學(xué)習(xí)更多技能
請點(diǎn)擊下方公眾號
![]()

