大家好!
本文主要是關於即將釋出的 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