React 18 通過其改進的渲染系統帶來了并發能力,并在此基礎上構建了轉換或自動批處理等性能增強特性。下面就看看到底有哪些值得關注的新特性。
迭代更新內容
總的來說,由于新的并發特性是漸進適配并按需啟用的,React 18 中的重大更改僅限于幾個簡單的 API 更改,以及對 React 中多個行為的穩定性和一致性的一些改進,比較重要的一點是,不再支持 IE 瀏覽器。
1、客戶端渲染 API
帶有 createRoot() 的 root API,替換現有的 render() 函數,提供更好的人體工程學并啟用新的并發渲染特性。
import { createRoot } from "react-dom/client";
import App from "App";
const container = document.getElementById("app");
const root = createRoot(container);
root.render(<App />);
請注意,這個新的 API 現在已從 react-dom/client 模塊導出,卸載和水合 API 也發生了變化。
// Unmount component at DOM node:
// ...
root.unmount();
// Hydration
import { hydrateRoot } from "react-dom/client";
// ...
const container = document.getElementById("app");
const root = hydrateRoot(container, <App tab="home" />);
由于使用 Suspense 時會出現不正確 timing 的問題,渲染回調已經一去不復返了。替代選擇是頂部組件內部的一個效果:
import { createRoot } from "react-dom/client";
import { useEffect } from "react";
import App from "App";
const App = () => {
useEffect(() => {
console.log("render callback");
});
return <div></div>;
};
const container = document.getElementById("app");
const root = createRoot(container);
root.render(<App />);
2、自動批處理
createRoot() API 還是 React 18 中另一個改進的入口——自動批處理。在 React 的早期版本中,狀態更新在 React 事件偵聽器中完成時已經批量處理了,以優化性能并避免重渲染。從 React 18 開始,狀態更新也將被安排到其他地方——比如在 Promise、setTimeout 回調和原生事件處理程序中。
const App = () => {
const handleClick = () => {
setA((a) => a + 1);
setB((b) => b - 1);
// Updates batched - single re-render
};
setTimeout(() => {
setA((a) => a + 1);
setB((b) => b - 1);
// New (v18): Updates batched - single re-render
}, 1000);
// ...
};
這個更改雖然一般來說符合人們期望,也挺有用,但可能是破壞性的。如果你的代碼依賴于在分開的狀態更新之間重渲染的組件,那么你必須使其適應新的批處理機制,或使用 flushSync() 函數來強制立即刷新更改。
import { flushSync } from "react-dom";
// ...
const handleClick = () => {
flushSync(() => {
setA((a) => a + 1);
});
// Re-render
flushSync(() => {
setB((b) => b - 1);
});
// Re-render
};
3、嚴格模式更新
React 18 帶來了大把新特性,此外還有很多新特性正在路上。為了讓你的代碼為此做好準備,StrictMode 變得更加嚴格了。最重要的是,StrictMode 將測試組件對可重用狀態的彈性,模擬一系列的掛載和卸載行為。它旨在讓你的代碼為即將推出的特性(可能以組件的形式)做好準備,這將在組件的掛載周期中保留這個狀態。
雖然它肯定會在未來提供更好的性能,但就目前而言,啟用 StrictMode 時必須要考慮這個事情。
除了以上提到的更改之外,根據你的 React 代碼庫,你可能還會發現其他一些更改。
值得一提的是,React 18 將不再支持 IE 瀏覽器,因為 React 18 現在依賴很多現代瀏覽器特性,如 Promise 或 Object.assign。
其余的更改與一些 React 行為的穩定性和一致性有關,不太可能影響你的代碼庫。
4、并發的 React
并發渲染器是 React 渲染系統的一項幕后特性。它允許并發渲染,即同時在后臺準備多個版本的 UI。這意味著更好的性能和更平滑的狀態轉換。
雖然并發似乎只是一個實現細節,但其實它是大多數新特性的動力源泉。事實上,只有當你使用其中一種特性(如 transition、Suspense 或流式 SSR)時,才會啟用并發渲染。這就是為什么了解并發渲染的工作機制是非常重要的。
5、Transition
Transition 是由并發渲染提供支持的新特性之一。它旨在與現有狀態管理 API 一起使用,以區分緊急和非緊急狀態更新。通過這種方式,React 知道哪些更新需要優先考慮,哪些更新需要在后臺通過并發渲染準備。
要知道何時使用 transition,你必須更好地了解用戶是如何與你的應交互的。例如,在字段中鍵入或單擊按鈕是用戶期望立即獲得響應的操作——響應可能是出現在文本字段中的一個值,或是要打開的某個菜單。但對于搜索、加載或處理數據(例如搜索欄、圖表、過濾表等)這些事情,用戶也會期望它們需要一些時間來完成。后者就是你使用 transition 的場景了。
可以使用 useTransition() 鉤子來創建一個 transition。這個鉤子返回一個函數來啟動一個 transition,還有一個掛起的指示器來通知你 transition 的進度。
import { useTransition, useState } from "react";
const App = () => {
const [isPending, startTransition] = useTransition();
const [value, setValue] = useState(0);
function handleClick() {
startTransition(() => {
setValue((value) => value + 1);
});
}
return (
<div>
{isPending && <Loader />}
<button onClick={handleClick}>{value}</button>
</div>
);
};
在 startTransition() 回調中提交的任何狀態更新都將被標記為 transition,從而使其他更新具有優先權。如果你不能使用這個鉤子,還有一個單獨的 startTransition() 函數可用——雖然它不會通知你轉換的進度。
import { startTransition } from "react";
// ...
startTransition(() => {
// Transition updates
});
// ...
6、Suspense 更新
與 React Suspense 結合使用時,transition 的效果是最好的。由于一些改進,Suspense 現在可以很好地與并發渲染集成、在服務器上工作,并且可能很快支持 lazy() 加載組件之外的用例。與 transition 一起使用時,Suspense 將避免隱藏現有內容??紤]以下示例:
import { Suspense } from "react";
// ...
const App = () => {
const [value, setValue] = useState("a");
const handleClick = () => {
setValue("b");
};
return (
<>
<Suspense fallback={<Loader />}>
{value === "a" ? <A /> : <B />}
</Suspense>
<Button onClick={handleClick}>B</Button>
</>
);
};
在狀態改變時,lazy() 加載的組件將觸發 Suspense,導致 fallback 元素的渲染。如果你將狀態更改標記為一個 transition,React 將知道它應該在后臺準備新視圖,同時仍保持當前視圖可見。
import { Suspense, useTransition } from "react";
// ...
const App = () => {
const [value, setValue] = useState("a");
const [isPending, startTransition] = useTransition();
const handleClick = () => {
startTransition(() => {
setValue("b");
});
};
return (
<>
<Suspense fallback={<Loader />}>
<div style={{ opacity: isPending ? 0.7 : 1 }}>
{value === "a" ? <A /> : <B />}
</div>
</Suspense>
<Button onClick={handleClick}>B</Button>
</>
);
};
現在,即使在處理 transition 時視圖不會改變,你仍然可以使用過渡指示器來向用戶提供反饋,例如設置容器不透明度。將上述改進與未來 Suspense 的新能力(與 lazy() 加載的組件之外的異步任務一起使用)相結合,意味著 Suspense 將成為 React 最強大的特性之一。
7、服務端渲染改進
除了 Suspense 支持之外,React 的 SSR 方面還有很多其他變化。將 Suspense 與 SSR 流式傳輸和懶惰水合(lazy hydration)相結合,意味著你的服務端渲染應用將盡快水合并可用。不僅如此,零打包體積的服務端組件即將到來。它們目前處于試驗階段,但可能會在以后的次要版本中進入穩定狀態。使用它們時,你將能減少提供給客戶端的 JS 代碼,甚至進一步優化 React 應用程序的性能和加載時間。
React 17 的多個更改,即使你的代碼庫很大,你也應該能夠輕松地逐步采用 React 18。你不僅可以在應用程序的選定部分中使用新版本,還可以從 render() 遷移到 createRoot(),來一步步選擇加入新的特性和行為。最重要的是,即使使用的是 createRoot(),你仍然可以逐步采用并發渲染,因為它只有在你使用它的特性時才會啟用??傮w而言,遷移過程應該很順利。
對 React 的展望
React 18 帶來了許多新特性,可以看到一些即將出現的新事物。服務器組件、用于數據獲取的 Suspense,和組件渲染都是接下來的新特性的一部分。React 正在與它的整個生態系統一起發展,迫不及待地想看看接下來會發生什么。