React 并發(fā) API 實戰(zhàn),這幾個例子看懂你就明白了
本文適合對React 并發(fā) API感興趣的小伙伴閱讀。
歡迎關注前端早茶,與廣東靚仔攜手共同進階~
目錄
-
什么是并發(fā) -
它和 React 有什么關系 -
中斷和切換是如何工作的 -
那 Suspense 呢? -
如何啟動 transition -
結束語
什么是并發(fā)
并發(fā)是一種執(zhí)行模型,它允許程序的不同部分可以不按順序執(zhí)行,而不影響最終結果。你可能聽說過多線程或多進程。由于瀏覽器中的 JavaScript 只能訪問一個線程(雖然 Web Workers 在單獨的線程中運行,但它們和 React 關系不大),我們不能使用多線程來并行處理一些計算。為了確保資源的最佳利用和頁面的響應性,JavaScript 必須采用不同的并發(fā)模型:協(xié)作式多任務。這聽起來可能有點復雜,但別擔心,你已經熟悉這個模型了,而且肯定用過。
它和 React 有什么關系
在 React 18 之前,React 中的所有更新都是同步的。如果 React 開始處理一個更新,它會完成它,不管你在干嘛(當然,除非你關閉了標簽頁)。即使這意味著忽略了此時發(fā)生的用戶事件,或者如果你有一些特別重的組件,頁面會凍結。對于較小的更新來說,這還好,但對于涉及渲染大量組件的更新(比如路由變化),它對用戶體驗產生了負面影響。
React 18 引入了兩種類型的更新:緊急狀態(tài)更新和 transition 狀態(tài)更新。默認情況下,所有狀態(tài)更新都是緊急的,這樣的更新不能被中斷。transition 是低優(yōu)先級的更新,可以被中斷。從現在起,我也將使用“高優(yōu)先級更新”和“低優(yōu)先級更新”來指代它們。
為了保持向后兼容性,默認情況下,React 18 的行為和之前的版本一樣,所有更新都是高優(yōu)先級的,因此不可中斷。要啟用并發(fā)渲染,你需要通過使用startTransition或useDeferredValue將更新標記為低優(yōu)先級。
中斷和切換是如何工作的
在渲染低優(yōu)先級更新時,React 在渲染完每個組件后會暫停,并檢查是否有高優(yōu)先級更新需要處理。如果有,React 會暫停當前渲染,切換到渲染高優(yōu)先級更新。處理完這些后,React 會返回到渲染低優(yōu)先級更新(或者如果它無效了,就丟棄它)。除了高優(yōu)先級更新,React 還會檢查當前渲染是否耗時過長。如果耗時過長,React 會將控制權還給瀏覽器,以便它可以重繪屏幕,避免卡頓和凍結。
由于 React 只能在組件之間暫停(它不能在組件中間停下來),所以如果你有一兩個特別重的組件,并發(fā)渲染幫助不大。如果組件渲染需要 300 毫秒,瀏覽器就會被阻塞 300 毫秒。并發(fā)渲染真正發(fā)揮作用的地方是當你的組件只是稍微慢一點,但它們的數量比較多,以至于總渲染時間相當長。
那 Suspense 呢?
你可能聽說過 CPU 密集型程序。這類程序大多數時間都在積極地使用 CPU 來完成它們的工作。我們之前提到的慢組件可以歸類為 CPU 密集型:為了更快地渲染,它們需要更多的資源。
與 CPU 密集型程序相反,還有 I/O 密集型程序。這類程序大部分時間都在與輸入輸出設備(比如磁盤或網絡)交互。在 React 中負責處理 I/O 的組件是 Suspense。
如果組件在低優(yōu)先級更新期間暫停,Suspense 的行為會有所不同。如果 Suspense 邊界內已經有內容顯示,React 不會像通常那樣處理暫停并顯示 fallback 內容,而是會暫停渲染,轉而處理其他任務,直到 Promise resolved,然后提交一個帶有新內容的完整子樹。這樣,React 避免了隱藏已經顯示的內容。如果組件在首次渲染期間暫停,將顯示 fallback 內容。
如何啟動 transition
啟動 transition 有幾種方法,最基本的是startTransition函數。你像這樣使用它:
import { startTransition, useState } from 'react'
const StartTransitionUsage = () => {
const onInputChange = (value: string) => {
setInputValue(value)
startTransition(() => {
setSearchQuery(value)
})
}
const [inputValue, setInputValue] = useState('')
const [searchQuery, setSearchQuery] = useState('')
return (
<div>
<SectionHeader title="Movies" />
<input placeholder="Search" value={inputValue} onChange={(e) => onInputChange(e.target.value)} />
<MoviesCatalog searchQuery={searchQuery} />
</div>
)
}
這里發(fā)生的事情是,當用戶在搜索輸入框中輸入時,我們像往常一樣更新狀態(tài)變量inputValue,然后調用startTransition,傳入一個包含另一個狀態(tài)更新的函數。這個函數會立即被調用,React 會記錄其執(zhí)行期間所做的任何狀態(tài)更改,并將它們標記為低優(yōu)先級更新。請注意,至少在 React 18.2 中,只能傳遞同步函數給startTransition。
所以在我們的示例中,我們實際上啟動了兩個更新:一個是緊急的(更新inputValue),另一個是 transition(更新searchQuery)。MoviesCatalog組件可能會使用 Suspense 來根據搜索查詢獲取電影,這將使該組件成為 I/O 密集型。此外,它還可以渲染相當長的一系列電影卡片,這可能使它也成為 CPU 密集型。有了 transition,這個組件在加載數據時不會觸發(fā) Suspense fallback(會顯示過時的 UI),在渲染長列表的電影卡片時也不會卡住瀏覽器。
需要注意的是,在 CPU 密集型組件的情況下,它們應該用React.memo包裹起來,否則即使它們的 props 沒有變化,它們也會在每次高優(yōu)先級渲染時重新渲染,這會影響你應用的性能。
startTransition是最基礎的函數,主要用于 React 組件之外。要從 React 組件內部啟動 transition,我們有一個更酷的版本:useTransitionhook。
import { useTransition, useState } from 'react'
const UseTransitionUsage = () => {
const onInputChange = (value: string) => {
setInputValue(value)
startTransition(() => {
setSearchQuery(value)
})
}
const [inputValue, setInputValue] = useState('')
const [searchQuery, setSearchQuery] = useState('')
const [isPending, startTransition] = useTransition()
return (
<div>
<SectionHeader title="Movies" isLoading={isPending} />
<input placeholder="Search" value={inputValue} onChange={(e) => onInputChange(e.target.value)} />
<MoviesCatalog searchQuery={searchQuery} />
</div>
)
}
有了這個 hook,你不需要直接導入startTransition;相反,你調用useTransition()hook,它會返回一個包含兩個元素的數組:一個 boolean 值,表示是否有任何低優(yōu)先級更新正在進行(從這個組件發(fā)起),以及你用來啟動 transition 的startTransition函數。
當你以這種方式啟動 transition 時,React 實際上會進行兩次渲染:一次高優(yōu)先級渲染,將isPending翻轉為 true,以及一次低優(yōu)先級更新,包含你傳遞給startTransition的實際狀態(tài)更改。所以要小心,用React.memo包裹“昂貴”的組件。
我們還有另一個新 hook 是useDeferredValue。如果相同的狀態(tài)在關鍵和重型組件中都使用,它就變得有用了。就像我們上面的例子一樣。多方便???這是你如何使用它:
import { useDeferredValue, useState } from 'react'
const UseDeferredValueUsage = () => {
const [inputValue, setInputValue] = useState('')
const searchQuery = useDeferredValue(inputValue)
return (
<div>
<SectionHeader title="Movies" isLoading={inputValue !== searchQuery} />
<input placeholder="Search" value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
<MoviesCatalog searchQuery={searchQuery} />
</div>
)
}
在低優(yōu)先級渲染中,和高優(yōu)先級首次渲染中,useDeferredValue會存儲傳遞的值,并立即返回它,所以inputValue和searchQuery將是相同的字符串。但在隨后的高優(yōu)先級渲染中,React 總是返回存儲的值。但它也會比較你傳遞的值和存儲的值,如果它們不同,React 會安排一個低優(yōu)先級更新。如果在低優(yōu)先級等待更新時,高優(yōu)先級這時更新了,值再次變化,React 會丟棄它,并安排一個帶有最新值的新的低優(yōu)先級更新。
使用這個 hook,你可以擁有同一狀態(tài)的兩個版本:一個用于關鍵組件,比如輸入字段(通常不能接受延遲),另一個用于像搜索結果這樣的組件(用戶習慣了更長的延遲)。
結束語
關注我,一起攜手進階
歡迎關注前端早茶,與廣東靚仔攜手共同進階~
參考資料
Ivan Akulov 的這個演講: https://3perf.com/talks/react-concurrency/
[2]react18并發(fā)指導: https://sinja.io/blog/guide-to-concurrency-in-react-18#what-is-concurrency
