目前 Concurrent 尚處於實驗階段,大部分文檔尚未被翻譯。我基於目前的官方文檔,對 Concurrent 做一些介紹。html
上一篇是關於React Suspense for Data的介紹。介紹了 Suspense for Data 模式和現有的數據請求方式的一些區別。你們閱讀本篇文章前,能夠先閱讀下這篇文章。react
一般咱們在更新狀態時,咱們但願在屏幕當即看到狀態的變化。可是在有些狀況下,咱們會但願屏幕上的狀態更新延遲。好比,從一個頁面切換到另外一個頁面時,另外一個頁面的代碼或者數據尚未加載,會顯示一個蒼白的 loading 頁,是使人沮喪的。在這種狀況下,咱們更願意在上一個頁面停留更長的時間。在之前的 React 中,實現會很困難。可是 Concurrent UI 模式的出現帶來了新的可能。git
爲何說 Concurrent 模式。帶來了更好的交互體驗。咱們來舉一個例子, 在github中點擊進入下一層的文件夾,並不會出現loading頁,而是將頁面保持在前一個狀態,當數據請求完成後,纔會顯示新的狀態。其實過多的loading,可能會形成頁面的閃爍,用戶體驗並非很好。github
在使用 Concurrent UI 模式出現前,在按下切換按鈕後,當前頁面的狀態當即消失,並當即出現加載態。用戶體驗並非很好。若是在請求數據響應時間較短的狀況下,能夠跳過中間的加載狀態,直接顯示下一個狀態的頁面,就行了。後端
使用 Concurrent UI 模式後,在請求數據響應時間較短的狀況下(小於timeoutMs
),咱們能夠跳過中間加載態,直接顯示新的狀態頁面。函數
React提供的新的內置Hook,useTransitions
,能夠實現這種模式。useTransitions
會返回兩個值,startTransition
以及isPending
post
startTransition
, 是一個函數。告訴React,React能夠延遲某個狀態的更新(延遲進入Suspense的掛起狀態,保持目前的狀態)。isPending
, 是一個布爾值,告訴咱們狀態是否正在過渡。而useTransitions
的配置項。timeoutMs
,則告訴React,咱們願意爲過渡等待的最大時間。性能
若是超過最大時間,則進入掛起狀態,顯示Suspense
的fallback
。可是若是過渡完成在超時前,則顯示前一個狀態,直到新狀態過渡完成。優化
// 使用useTransition的例子
function Page () {
const [startTransition, isPending] = useTransition({
timeoutMs: 2000
});
const [resource, setResource] = useState(
initResource
);
const handleClickNext = () => {
startTransition(() => {
const nextId = getNextId()
setResource(http(nextId))
})
}
return (
<React.Fragment>
<button
disabled={isPending}
onClick={handleClickNext}
>Next Id</button>
<p>{isPending ? " Loading..." : null}</p>
<React.Suspense fallback={<h1>Loading...</h1>}>
<Figure resource={resource}/>
</React.Suspense>
</React.Fragment>
)
}
function App() {
return (
<div className="App">
<Page/>
</div>
)
}
複製代碼
因爲舊的狀態組件出如今屏幕,因此咱們知道它是存在的。對於新的狀態組件,它也存在在某處。使用startTransition
對狀態更新進行包裝。狀態更新將會發咱們看不到的地方(相似平行宇宙的地方)。當新的狀態準備完成,新舊狀態會發生合併,渲染出如今屏幕上。spa
雖然新舊狀態的組件是同時存在的,可是這不意味着它們是同時渲染的。計算機的並行計算,實際上是在極短的時間內,切換到不一樣的任務進行計算。
function UserList ({ resource }) {
const list = resource.read();
return (
<ul> { list && list.map(item => <li key={item.id}>{item.name}</li>) } </ul>
)
}
function App() {
const [resource, setResource] = useState(initResource);
const handleClickRefresh = () => {
setResource(http())
}
return (
<div className="App">
<button onClick={handleClickRefresh}>刷新</button>
<hr/>
<React.Suspense fallback={<h4>loading……</h4>}>
<UserList resource={resource}/>
</React.Suspense>
</div>
)
}
複製代碼
當咱們瀏覽一個頁面,並與之交互時,若是出現了沒必要要的loading,這種體驗是不愉悅的。咱們可使用 useTransitions
將狀態更新,包裝在 startTransition
中。點擊刷新,不會出現煩人的loading。
const initResource = http()
function List ({ resource }) {
const list = resource.read();
return (
<ul> { list && list.map(item => <li key={item.name}>{item.name}</li>) } </ul>
)
}
function App() {
const [resource, setResource] = useState(initResource);
const [startTransition, isPending] = useTransition({
timeoutMs: 2500
});
const handleClickRefresh = () => {
startTransition(() => {
setResource(http())
})
}
return (
<div className="App">
<button onClick={handleClickRefresh}>刷新 { isPending && '加載中……' }</button>
<hr/>
<React.Suspense fallback={<h4>loading……</h4>}>
<List resource={resource}/>
</React.Suspense>
</div>
)
}
複製代碼
如今感受好多了,點擊刷新,不會出現蒼白的loading頁,數據正在內部加載,當數據準備好,它就會顯示出來。
Transitions 是很常見的,任何能夠致使組件被掛起的組件都應該被 useTransition
包裹起來。下面是一個 Button
組件的例子。經過將 Transitions 融合到組件設計中,能夠避免大量重複無用的代碼。
// Button組件
function Button (props) {
const [startTransition, isPending] = useTransition({
timeoutMs: 3000
});
const { onClick, children } = props;
const handleClick = () => {
startTransition(() => {
onClick()
})
}
return (
<button onClick={handleClick}> { children } { isPending && '……s' } </button>
)
}
複製代碼
// 在App文件中,不須要再次重複useTransition的邏輯
function App() {
const [resource1, setResource1] = useState(initResource);
const [resource2, setResource2] = useState(initResource);
const handleRefresh1 = () => {
setResource1(http())
}
const handleRefresh2 = () => {
setResource2(http())
}
return (
<div className="App">
<Button onClick={handleRefresh1}>刷新列表1</Button>
<br/>
<Button onClick={handleRefresh2}>刷新列表2</Button>
<hr/>
<React.Suspense fallback={<h4>loading……</h4>}>
<List resource={resource1}/>
</React.Suspense>
<hr/>
<React.Suspense fallback={<h4>loading……</h4>}>
<List resource={resource2}/>
</React.Suspense>
</div>
)
}
複製代碼
useTransitions
,會時頁面保持在當前的狀態,使頁面依然是可交互的。當數據準備完畢,進入Skeleton態。若是數據超時,則回退到Receded態。默認狀況下,咱們的頁面狀態變化是 Receded -> Skeleton -> Complete
。可是 Receded
狀態給用戶的體驗並很差,在使用 useTransitions
後。咱們首選的頁面狀態變化是 Pending -> Skeleton -> Complete
。
考慮下面這種狀況,假設咱們的 "用戶列表" 的接口響應速度老是很慢(須要5s的時間才能返回),後端同窗短期也沒法優化。它會拖慢咱們整個頁面進入 Skeleton
態的時間。讓頁面長期處於 Pending
態,遲遲不能進入下一個狀態。(以下圖所示)
function HomePage ({ resource }) {
return (
<React.Fragment>
{/* UserList列表加載過慢 */}
<UserList resource={resource}/>
<NewsList resource={resource}/>
</React.Fragment>
)
}
function LoginPage ({ onClick }) {
return (
<div>
<h1>首頁</h1>
{/* Button組件使用了useTransitions進行封裝 */}
<Button onClick={onClick}>下一頁</Button>
</div>
)
}
function App() {
const [tab, setTab] = useState('login')
const [resource, setResource] = useState(null);
const handleClick = () => {
setResource(http())
setTab('home')
}
let page = null
if (tab === 'login') {
page = <LoginPage onClick={handleClick}/>
} else {
page = <HomePage resource={resource}/>
}
return (
<React.Suspense fallback={<h1>loading……</h1>}>
<React.Fragment>
{page}
</React.Fragment>
</React.Suspense>
)
}
複製代碼
咱們可能會首先想到,修改 useTransitions
的 timeoutMs
, 但這樣一樣無濟於事,由於回退到 Receded
一樣是很差的體驗。
有沒有什麼好的辦法,能夠優化呢?咱們能夠將慢速的組件,包裹在Suspense中,讓其延遲加載,這樣看起來好多了,頁面不會長期停留在Pending
態。
function HomePage ({ resource }) {
return (
<React.Fragment>
{/* 使用Suspense對慢速組件進行包裹 */}
<React.Suspense fallback={<h4>用戶列表加載中……</h4>}>
<UserList resource={resource}/>
</React.Suspense>
<NewsList resource={resource}/>
</React.Fragment>
)
}
複製代碼
試想一下。當咱們已經在 Skeleton
態時(這是合併渲染的前提)。此時有兩個響應將在短期內依次返回,好比用戶列表在 1200ms 後返回,新聞列表在 1300ms 後返回。兩個Suspense
將會依次結束掛起的狀態(兩個Suspense
是嵌套的)。咱們已經等待了 1200ms, 也不在意多等待100ms,因此爲了減小頁面的重繪次數,提高性能。React會合並它們,一塊兒渲染,而不是兩個列表組件依次渲染。可是若是間隔大於100ms,仍是會依次渲染。
小於等於100ms
function HomePage ({ resource }) {
return (
<React.Fragment>
{/* Title resource 在 1000ms後響應,頁面進入 Skeleton 態,這是合併渲染的前提 */}
<Title resource={resource}/>
{/* News resource 在 1200ms後響應 */}
{/* NewsList 將等待 UserList 響應後一塊兒渲染 */}
<React.Suspense fallback={<h4>加載信息……</h4>}>
<NewsList resource={resource}/>
{/* News resource 在 1300ms後響應 */}
<React.Suspense fallback={<h4>加載用戶列表……</h4>}>
<UserList resource={resource}/>
</React.Suspense>
</React.Suspense>
</React.Fragment>
)
}
複製代碼
大於100ms
不是全部的狀態更新,都適合放在 useTransition
中。咱們首先來看一下谷歌翻譯的例子。
在谷歌翻譯中。咱們在左側每輸入一點內容,右側都會給出翻譯的結果。當左側結束輸入的時候,右側會給出完整的翻譯結果。咱們來試着還原下這個效果。
在第一次輸入的時候,Translation
組件被掛起,頁面顯示 Suspense
的 fallback
。這種效果是不理想的,咱們應該在輸入完成前,看到以前翻譯的內容。
而且控制檯會打印中,以下的警告
警告咱們更新,更新應該分爲多個部分。一部分更新須要及時的反饋到頁面上,而另外一部分更新應該包含在 Transition
function App () {
const [query, setQuery] = useState(initQuery)
const [resource, setResource] = useState(initResource)
const handleChange = (e) => {
const query = e.target.value
setQuery(query);
setResource(http(query))
}
return (
<div>
<input value={query} onChange={handleChange}/>
<React.Suspense fallback={<h1>翻譯中…………</h1>}>
<Translation resource={resource} />
</React.Suspense>
</div>
)
}
複製代碼
若是咱們把全部的狀態更新,都包含在 Transition
中呢?問題更大了,頁面的更新將會變得很是緩慢。input
中value的變化鬥都很是的卡頓。
function App () {
const [query, setQuery] = useState(initQuery)
const [resource, setResource] = useState(initResource)
const [startTransition, isPending] = useTransition({
timeoutMs: 5000
})
const handleChange = (e) => {
startTransition(() => {
const query = e.target.value
setQuery(query)
setResource(http(query))
})
}
return (
<div>
<input value={query} onChange={handleChange}/>
<React.Suspense fallback={<h1>翻譯中…………</h1>}>
<Translation resource={resource} />
</React.Suspense>
</div>
)
}
複製代碼
正確的作法是應該區分,高優先級狀態的更新(setQuery
),以及低優先級的狀態更新(setResource
)。setQuery
是當即發生的,setResource
則須要過渡。
function App () {
const [query, setQuery] = useState(initQuery)
const [resource, setResource] = useState(initResource)
const [startTransition, isPending] = useTransition({
timeoutMs: 5000
})
const handleChange = (e) => {
const query = e.target.value
setQuery(query)
startTransition(() => {
setResource(http(query))
})
}
return (
<div>
<input value={query} onChange={handleChange}/>
<React.Suspense fallback={<h1>翻譯中…………</h1>}>
<Translation resource={resource} />
</React.Suspense>
</div>
)
}
複製代碼
useTransition
必須配合 Suspense
一塊兒使用。startTransition
中的操做能夠觸發Suspense
,纔會讓頁面進入 Pending
態。startTransition
,所觸發的Suspense
,必須在startTransition
觸發前掛載完成。(Suspense
必須提早包裹在startTransition
操做的外面)useTransition
中的狀態更新,不該該包含高優先級的(須要及時更新的內容。)思考一個例子,咱們在一個頁面中會請求兩個接口,一個文章的接口,一個文章的留言接口。兩個接口響應時間是隨機的。這就意味着,可能文章的留言接口已經返回了,可是文章的接口尚未返回。給用戶的視覺體驗並很差。
解決這種問題,有兩種思路,第一種是將,文章的接口和文章的留言接口,都放在同一個 Suspense
中。可是若是文章的接口提早返回,咱們沒有理由去等待留言的接口返回後,而後再渲染頁面。
<React.Suspense fallback={<h1>加載中……</h1>}>
<Article resource={resource}/>
<ArticleComments resource={resource}/>
</React.Suspense>
複製代碼
更好的辦法是使用 SuspenseList
, SuspenseList
會控制 Suspense
子節點的顯示順序。
當SuspenseList的revealOrder屬性設置爲forwards時,內容將會按照它們在VDOM樹中的順序顯示,從前向後渲染,即便它們的數據以不一樣的順序到達。(若是前面返回時間,大於後面的,它們會一塊兒顯示)
function App () {
return (
<React.Fragment>
<React.SuspenseList revealOrder="forwards">
<React.Suspense fallback={<h1>文章加載中……</h1>}>
<Article resource={resource}/>
</React.Suspense>
<React.Suspense fallback={<h1>留言加載中……</h1>}>
<ArticleComments resource={resource}/>
</React.Suspense>
</React.SuspenseList>
</React.Fragment>
)
}
複製代碼
當設置爲backwards時,內容將會按照它們在VDOM樹中的順序顯示,從前向後渲染
當設置爲together時,內容會一塊兒渲染。
另一點是,SuspenseList是能夠進行組合的。
上面僅是做者本身的理解,若有錯誤請及時指出。