大家好,我是若川。持續組織了8個月源碼共讀活動,感興趣的可以點此加我微信 ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列
這篇文章發布于3.28日,3.29日 react 18 發布。
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-rendersetCount(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 的到來。
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經堅持寫了8年,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助3000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
掃碼加我微信 ruochuan02、拉你進源碼共讀群
今日話題
略。分享、收藏、點贊、在看我的文章就是對我最大的支持~