大家好!
本文主要是關於即將發佈的 react 18 的新特性。那麼 react18 帶來了什麼呢?
詳情可以關注 github React 18 工作組倉庫
1. automatic batching:自動批處理。
batching 批處理,說的是,可以將回調函數中多個 setState 事件合併爲一次渲染,因此是異步的。
解決的問題是多次同值、不同值 setState, 期望最後顯示的是最後一次 setState 的結果,減少渲染。
const Index = () => {
const [name, setName] = useState('')
const [age, setAge] = useState(0)
const change = () => {
setName('a')
setAge(1)
// 僅觸發一次渲染,批處理,2次setState合併爲一次渲染
// 需需要立即重渲染,需要手動調用
// ReactDOM.flushSync(() => {
// setName('a') // 立即執行渲染
// setAge(1) // 立即執行渲染
// // 不會合並處理,即沒有批處理,觸發2次
// });
}
console.log(1) // 只打印一次
return (
<div>
<p>name: {name}</p>
<p>age: {age}</p>
<button onClick={change}>更改</button>
</div>
)
}
但是 react 18 之前,在 promise、timeout 或者 event 回調中調用多次 setState,由於丟失了上下文,無法做合併處理,所以每次 setState 調用都會立即觸發一次重渲染:
const Index = () => {
const [name, setName] = useState('')
const [age, setAge] = useState(0)
const change = () => {
setTimeout(() => {
setName('a') // 立即執行渲染
setAge(1) // 立即執行渲染
// 不會合並處理,即沒有批處理,觸發2次
// 若需要批處理,需要手動調用
// ReactDom.unstable_batchedUpdates(() => {
// setName('a')
// setAge(1)
// // 合併處理
// })
// 並且將 ReactDOM.render 替換爲 ReactDOM.createRoot 調用方式
// 舊 ReactDOM.render(<App tab="home" />, container);
// 新 ReactDOM.createRoot(container).render(<App tab="home" />)
}, 0);
}
console.log(1) // 打印2次
return (
<div>
<p>name: {name}</p>
<p>age: {age}</p>
<button onClick={change}>更改</button>
</div>
)
}
react18,在 promise、timeout 或者 event 回調中調用多次 setState,會合併爲一次渲染。提升渲染性能。
v18實現「自動批處理」的關鍵在於兩點:
- 增加調度的流程
- 不以全局變量 executionContext 爲批處理依據,而是以更新的「優先級」爲依據
參考:
2. concurrent apis:全新的併發 api。比如:startTransition
Concurrent:併發,採用可中斷的遍歷方式更新 Fiber Reconciler。是漸進升級策略的產物。
不同更新觸發的視圖變化是有輕重緩急的,讓高優更新對應的視圖變化先渲染,那麼就能在設備性能不變的情況下,讓用戶更快看到他們想看到的UI。
案例:用戶操作滑塊,然後響應樹的變化。滑塊響應是高優先級的,而樹的變化可以認爲是低優先級的。
未開啓:可以看到滑塊的拖動有卡頓
開啓:可以看到滑塊的拖動,非常的絲滑順暢
代碼實現,將設置更新樹的 setState,放到 startTransition 中。而更新滑塊的不變,認爲是高優先級,優先響應。
2部分:
- 緊急響應:滑塊。
- 過渡更新:根據滑塊,呈現結果內容。
import { useTransition } from 'react';
const [isPending, startTransition] = useTransition();
// 更改滑塊觸發
function changeTreeLean(event) {
const value = Number(event.target.value);
setTreeLeanInput(value); // 更新滑塊
// 是否開啓startTransition
if (enableStartTransition) {
startTransition(() => {
setTreeLean(value); // 這個變慢,根據滑塊,呈現結果內容。
});
// react18之前,想要有類似功能。變體,setTimeout,防抖節流
// setTimeout(() => {
// setTreeLean(value)
// }, 0)
} else {
setTreeLean(value);
}
}
// 過渡期間可以這麼處理
{isPending ? <Spinner /> : <Con>}
比 setTimeout
更好,能有狀態 isPending
,且更早更快的呈現更新到界面上(微任務裏處理)。而且 setTimeout
是不可中斷的,而 startTransition
是可中斷的,不會影響頁面交互響應。
依賴於React底層實現的優先級調度模型,被 startTransition 包含的 setState 的優先級會被設置爲低優先級的過渡更新。
參考:
- 真實世界示例:爲慢速渲染添加 startTransition
- 新功能:startTransition
- React 18不再依賴Concurrent Mode開啓併發了
- 給女朋友講React18新特性:startTransition
- A better React 18 startTransition demo
3. suspense:更好的 suspense。更好的支持在 ssr 和 異步數據 場景下使用 suspense。
1. ssr 下支持,可參考:React18 中的新 Suspense SSR 架構
2.透明的異步數據處理(未來18.x支持)
和寫同步邏輯代碼一樣,寫異步代碼邏輯。大大的簡化了代碼邏輯的書寫。把代數效應應用到極致了,把異步的副作用剝離了。
代數效應是函數式編程中的一個概念,用於將副作用從函數調用中分離。
場景案例:demo,顯示暢銷書排行榜。
其中,名稱和日期是一個接口獲取,而下面的列表是另一個接口獲取。
從圖中,可以明顯感到 with suspense 的效果更絲滑,用戶體驗更好。而代碼也非常簡潔。部分代碼如下:
```js
// 接口部分
import { fetch } from "react-fetch"
export function fetchBookLists() {
const res = fetch(`
https://api.nytimes.com/svc/books/v3/lists/names.json?api-key=${API_KEY}`)
const json = res.json()
if (json.status === "OK") {
return json.results
} else {
console.log(json)
throw new Error("Loading failed, likely rate limit")
}
}
// 組件部分
// 沒有處理 loading 狀態等的異步處理,和同步已經完全一致的代碼書寫
const Content = () => {
const list = fetchBookLists()[0]
return (
<>
<h4>From {list.display_name}</h4>
<Paragraph sx={{ mt: -3 }}>
Published on {list.newest_published_date}
</Paragraph>
<BookList list={list} />
</>
)
}
export const BestSellers = () => {
return (
<Suspense fallback={<Spinner />}>
{/* loading must happen inside a <Suspense> */}
<Content />
</Suspense>
)
}
```
而在 react18 之前,你得這麼寫:
```js
// 接口部分
import { fetch } from "react-fetch"
export async function fetchBookLists() {
const res = await fetch(`
https://api.nytimes.com/svc/books/v3/lists/names.json?api-key=${API_KEY}`)
const json = await res.json()
if (json.status === "OK") {
return json.results
} else {
console.log(json)
throw new Error("Loading failed, likely rate limit")
}
}
// 組件部分,按照異步的邏輯寫,寫loading,對異步結果的處理等
function useNYTBestSellerLists() {
// poor man's useQuery implementation
const [isLoading, setIsLoading] = useState(false)
const [lists, setLists] = useState(null)
useEffect(() => {
setIsLoading(true)
fetchBookLists()
.then((lists) => {
setLists(lists)
setIsLoading(false)
})
.catch(() => setIsLoading(false))
}, [])
return { isLoading, lists }
}
export const BestSellers = () => {
const { isLoading, lists } = useNYTBestSellerLists();
if (isLoading) {
return <Spinner />;
}
if (!lists) {
return "not loading or error";
}
const list = lists[0];
return (
<>
<h4>From {list.display_name}</h4>
<Paragraph sx={{ mt: -3 }}>
Published on {list.newest_published_date}
</Paragraph>
<BookList list={list} />
</>
);
}
```
參考:
3.優化 suspense 的行爲表現。
場景舉例:
<Suspense fallback={<h3>loading...</h3>}>
<LazyCpn /> // 爲 React.lazy 包裹的異步加載組件
<Sibling /> // 普通組件
</Suspense>
由於 Suspense 會等待子孫組件中的異步請求完畢後再渲染,所以當代碼運行時頁面首先會渲染 fallback:loading。而在loading這個過程中,頁面表現是一致的,但是背後的行爲是不一致的:
- react18 之前:即在 Legacy Suspense 中,Sibling 組件會立即安裝到 DOM 並觸發其效果/生命週期。頁面上隱藏。
- react18:即在 Concurrent Suspense 中,Sibling 組件沒有掛載到 DOM。它的效果/生命週期也不會在 ComponentThatSuspends 解決之前觸發。
react18,Sibling 不會執行,會等 suspense 包裹的組件都加載完才執行渲染
優化的是提交渲染的流程:
打斷兄弟組件並阻止他們提交。等待提交 Suspense 邊界內的所有內容- 掛起的組件及其所有兄弟組件 - 直到掛起的數據解決。然後在一個單一的、一致的批次中同時提交整個樹渲染。
參考:
4. 其他
比如:新 Hook —— useId
解決問題:ssr 場景下,客戶端、服務端生成的id不匹配!官方推出 Hook——useId解決,每個 id 代表該組件在組件樹中的層級結構。
function Checkbox() {
// 生成唯一、穩定id
const id = useId();
return (
<>
<label htmlFor={id}>Do you like React?</label>
<input type="checkbox" name="react" id={id} />
</>
);
);
參考:爲了生成唯一id,React18專門引入了新Hook:useId
最後
這幾個重大的更新,目的都是較少渲染、根據優先級響應、提升性能、擁有更好的體驗。非常值得期待。
想嚐鮮的可安裝 react18 beta 版(2021-11-16發佈的)
# npm
npm install react@beta react-dom@beta
# yarn
yarn add react@beta react-dom@beta