React 18 帶給我們的驚喜

語言: CN / TW / HK

1、前言

React 18 的 alpha 版已經發布有段時間了,之前學習後由於沒有開發實踐結合去思考,對 React 18 的意義認識並不深刻。前段時間做了一些老舊專案遷移,發現複雜專案下每次渲染都要精心調整,否則就會有麻煩的效能或體驗瑕疵,而 React 內部渲染順序和優先順序很難調整,就導致總體體驗差了點意思。回顧了 React 18 的三個新特性,有種久旱逢甘雨的欣喜。

團隊內部推行了 React hook,好處就不在這裡贅述了,也陸續收到了一些負面反饋。其一就是 React hook 更加趨向面向資料實體進行拆分,而一個動作需要多個數據實體協作,例如一個 Modal Form 需要 visible 和 data 兩個資料項協作,但是這兩個資料項的變更會觸發兩次渲染結算,增加效能開銷。

作者之前遇到過複雜 Form 表單下,初次渲染由於資料項過於複雜導致無限次 render 的 bug。在這個 case 中,核心的衝突就是在資料項複雜度提升的同時,React Diff 的效能就遇到了“偽瓶頸”。這裡不是說 React Diff 效能差,僅僅想表達它的高效能需要更高的設計理念和實踐經驗,這也是相對於 Vue 等更加易學的框架而言,總的來說上限高下限也低。而 React 18 的變化讓我看到了 React 團隊正在關注這一部分,並且給予了更好的解決方案。

閒聊到此為止,進入正題,給大家介紹下 React 18 的四個重要新特性:

  • Automatic batching

  • Concurrent APIS

  • SSR for Suspense

  • New Render API

2、Automatic batching

在 React 中使用 setState 來進行 dispatch 元件 State 變化,當 setState 在元件被呼叫後,並不會立即觸發重新渲染。React 會執行全部事件處理函式,然後觸發一個單獨的 re-render,合併所有更新。這裡舉個簡單例子:

const [count, setCount] = useState(0);

function increment() {
// setCount(count + 1)
// 使用無狀態函式進行優化,避免多次 re-render
setCount(c => c + 1);
}

function handleClick() {
increment();
increment();
increment();
}

最終 React 會將更新函式放到一個佇列裡,然後合併佇列觸發 setCount (3) 的 re-render,這就是 batching 的含義。

這樣既可以減少程式資料狀態存在中間值導致的不穩定性,也可以提升渲染效能。但是可惜的是在 React 18 之前,如果在回撥函式的非同步呼叫中,執行 setState,由於丟失了上下文,無法做合併處理,所以每次 setState 呼叫都會觸發一次 re-render。

function handleClick() {
// React 18 以前的版本
(/*...*/).then(() => {
setCount((c) => c + 1); // 立刻重渲染
setFlag((f) => !f); // 立刻重渲染
});
}

而 React 18 帶來變化便是,任何情況下都可以合併渲染了!如果你希望在 React 18 的 setState 後立即執行重新渲染, 只需要使用 flushSync 包裹即可。

function handleClick() {
// React 18+
fetch(/*...*/).then(() => {
ReactDOM.flushSync(() => {
setCount((c) => c + 1); // 立刻重渲染
setFlag((f) => !f); // 立刻重渲染
});
});
}

迴歸到實際開發中,Automatic batching 機制讓我們有能力對渲染順序和節奏進行一些基礎的把控。例如在 Canvas 畫布編輯場景中,我們可以載入完主節點框架之後立刻進行渲染,而每個節點的內容則可以進行合併渲染,儘可能加快使用者看到可編輯頁面的時間,同時避免 http 非同步函式引起的頻繁渲染的效能開銷。

3、Concurrent APIS

在官方視訊中明確指出了 React 18 中並不存在 Concurrent Mode,只有用於併發渲染的併發新特性。開發者希望能夠在 Web Platform 引入併發渲染,來實現多個渲染任務的並行渲染,其中 Suspense 就是基於此誕生的。

React 18 提供了三個新的 API 支援這一併發特性,分別是:

  • startTransition()

  • useDeferredValue()

  • useTransition()

由於 useTransition 的官方文件並未放出來,這裡就僅僅介紹另外兩種 API。

3.1 startTransition()

import { startTransition } from "react";

// 緊急更新:
setInputValue(input);

// 標記回撥函式內的更新為非緊急更新:
startTransition(() => {
setSearchQuery(input);
});

簡單來說,被 startTransition 包裹的 setState 觸發的渲染被標記為不緊急渲染,意味著他們可以被其他緊急渲染所搶佔。這種渲染優先順序的調整手段可以幫助我們解決各種效能偽瓶頸,提升使用者體驗。

3.2 useDeferredValue()

這個 hook 適用於設定延遲值,參考官方演示視訊來看。

function Page() {
const [filters, mergeFilter] = useMergeState(defaultFilters);
const deferedFilters = React.useDeferedValue(filters);
return (
<Fragment>
<Filters filters={filters} >
<List filters={deferedFilters} >
</Fragment>
);
}

useDeferedValue () 會將 List 元件的渲染變得更加平滑,深層次來看則是 defered value 引起的渲染則會被標記為不緊急渲染,會被 filters 引起的渲染進行搶佔,進而達到使用者快速輸入搜尋等場景下頁面抖動或者卡頓問題。

4、SSR for Suspense

早在 2018 年,React 就推出了 Suspense 的基礎版本。

它可以在客戶端動態載入程式碼(React.lazy),配合 Suspense 元件實現資料拉取和狀態控制的關注點分離(當子元件未載入完成時,父元件填充 fallback 宣告的元件),但是並不能在伺服器端進行載入。

<Suspense fallback={<Skeleton />}>
<Header />
<Suspense fallback={<ListPlaceholder />}>
<ListLayout />
</Suspense>
</Suspense>

React 的開發者對 Suspense 的期望並不僅僅止步於此,他們認為 Suspense 拓展了我們對元件的概念。在 React 18 中,Suspense 可以執行在伺服器端,Server Rendering 的效能不需要受制於效能最差的元件(木桶效應)。

在 React 18 之前,Server Rendering 的流程是伺服器端請求所有資料,然後傳送 HTML 到客戶端或者說瀏覽器,然後由客戶端的 hydrate 內容,每個環節必須按部就班的執行。當 Suspense 可以在伺服器端使用之後,一旦某個元件載入慢,就可以將 fallback 的內容傳輸到客戶端(例如下圖中的 loading 態),保證使用者儘可能早的可進行互動。

更加優秀的部分則是,hydrate 是可以通過使用者的行為來調整優先順序的,例如上圖中 Profile 元件和正在 Loading 的評論元件同時處於 Suspense 的流程中,此時使用者點選評論元件,React 將會優先 hydrate 評論元件,儘可能優先滿足使用者互動體驗。

迴歸到程式碼實現細節,整體框架上伺服器和客戶端的連線必然趨向於持續性的長連結,因此 res.send 需要變成 res.socket,pipeToNodeWritable 替換 renderToString 並且配合 Suspense 即可官方例子 )。

5、New Render API

新的更加友好的語義化 render 方式。

const container = document.getElementById("app");

// 舊 render API
ReactDOM.render(<App />, container);

// 新 createRoot API
const root = ReactDOM.createRoot(container);
root.render(<App />);

Client 端提供了新 水合 Hydrate API。

const root = ReactDOM.hydrateRoot(container, <App tab="home" />);

以及 新 useId () API 來為元件生成唯一 ID。

由於 Suspense 和 併發渲染在 React 18 的大規模使用,一些具有 External stores 的 API,比如全域性變數、document 物件如何在併發場景下保證一致性呢?如果無法保證一致性,在併發渲染過程中可能會導致元件展示的不一致。

為了解決這個問題,React 18 提供了 useSyncExternalStore() 這個 hook,來保證獲取 External stores 的一致性。

useSyncExternalStore(
// 註冊回撥函式
subscribe: (callback) => Unsubscribe,
// 獲取快照函式
getSnapshot: () => state
) => state

具體使用方式參考:

6、React 未來展望

在官方視訊中,開發者們也對未來版本的內容進行介紹。

Support for Data Fetching API

由於 Suspense 的大規模應用,其資料獲取變得更加定製化,目前常見的有 Relay、React Query 等。React 官方也希望將這一部分納入到 React 的 API 中。

Server Component

元件不僅可以通過網路讀取資料、也可以後臺數據層直接讀取服務資料,將大大減少伺服器端向客戶端傳輸的程式碼量,和同構模式十分類似。

React 18 in React Native

2022 年 React 18 將和 React Native 一起釋出,跨平臺構建的史詩級更新,RN 併發的一些老大難將得到解決。

7、結語

結合起來看,React 18 關注點在於更快的效能、使用者互動響應效率和跨平臺構建,其設計理念處處包含了中斷與搶佔概念。React 18 給我們提供了一些從應用構建視角下的手段,例如:

  • 在 Client 端隨時中斷的框架設計,第一優先順序渲染使用者最關注的 UI 互動模組。

  • 從後端到前端 “順滑” 的管道式 SSR,並將 hydration 過程按需化,且支援被更高優先順序使用者互動行為打斷,第一優先水合使用者正在互動的部分。

作為一名前端開發,十分期待 React 18 的到來。

  -   E N D   -

3 6 0 W 3 C E C M A T C 3 9 L e a d e r