hello 大家好,我是 superZidan,這篇文章想跟大家聊聊 React 編譯器 ,如果大家遇到任何問題,歡迎 聯繫我 或者直接微信添加 superZidan41
🔥🔥🔥 前方高能,乾貨滿滿,建議點贊➕關注➕收藏;
React 19 和 React 編譯器(此前稱作React Forget)最近一個月成為了 React 社區熱議的焦點。大家都對於可能很快就不必再在 React 中糾結於記憶化技術(memoization) 的問題感到異常激動(這是件好事)。但這種説法準確嗎?在未來幾個月內,我們真的可以開始拋棄 memo、useMemo和useCallback這些概念嗎?而當 React 編譯器正式推出後,又會帶來哪些實質性的變化?我們又應該如何學習 React 的新知識呢?
我們來深入探討一下。
React 19 不是 React 編譯器
讓我們首先澄清一個最關鍵的誤區:記憶化技術(Memoization)在短期內仍將是 React 開發的重要部分,因此現在還不是拋棄它的時候。需要明確的是,React 19 和 React 編譯器是兩件不同的事物。React 團隊在他們宣佈即將發佈 React 19 的同一篇博文中提到了編譯器,這讓許多人誤以為二者是相同的,誤解紛紛產生。
不過,React 團隊的一位成員通過一條推文對這個誤解進行了澄清,他説明了React 19 與 React Compiler之間的區別。這條推文幫助大家理解了 React 的最新動態,避免了進一步的混淆
在 React 19 版本中,我們期待引入多項新功能,但對於 React 編譯器的推出,則需要再耐心等待一段時間。目前尚不明確具體需要等待多久,但根據 React 核心團隊另一位成員的推文透露,編譯器可能在本年度末前推出。這一消息讓我們對 React 的未來發展充滿期待。
就我個人而言,我對這個時間表持懷疑態度。 如果我們看一下 React 團隊成員介紹編譯器及其時間表的演講,我們正處於編譯器之旅的中間位置:
這段開發之旅始於2021年,已經兩年了。在像 Meta 這樣的龐大代碼庫中實施這種基礎性的變革無疑是一項巨大的挑戰,從時間線的中段跳躍到最終實現可能還需要再多兩年的時間。
不過,誰能確切知曉呢?也許 React 團隊真能在今年完成發佈,那無疑是個振奮人心的好消息。根據視頻中的介紹,Compiler 一個主要的能力是我們在採用它時無需修改現有代碼——它將會“即插即用”。如果 Compiler 確實能在年底前發佈,那將強有力地證明這一點,我們大多數人將能夠迅速、輕鬆地進行切換。
然而,即便 Compiler 今年發佈,並且確實非常容易的使用而且無任何副作用,這並不意味着我們可以立刻忘掉 useCallback和 memo 的使用。總會有一個過渡期,在這個期間,我們最初會討論 「如果你已經啓用了 Compiler」 的情況,然後逐漸過渡到「如果你還沒有遷移到 Compiler」的較為少見的情況。
從類組件轉向使用 hooks 的函數組件,這一心智模式的轉變我認為至少需要 3 年時間(從2018年起)——當所有教程、文檔和博客文章都更新之後,大多數人轉向了使用 hooks 的 React 版本,並且我們開始默認討論函數組件和 hooks。即使在 6 年後的今天,我們仍然可以在不同的地方找到許多類組件。
如果對 Compiler 我們採取相似的時間線預測,那意味着我們至少在未來 3 年內還需要保持對memo、useMemo和 useCallback 這些知識的掌握。如果你足夠幸運能夠在 Compiler 一發布就遷移到一個現代化的代碼庫,那麼你可能需要較短的時間來適應。但如果你是 React 的教學者,或者在一個遷移速度較慢、充斥着大量舊代碼的大型代碼庫中工作,那麼你可能需要更長的時間來適應。
React Compiler帶來了什麼變化?
所以,React Compiler 究竟會帶來什麼樣的改變呢?簡而言之,它將實現代碼的全面記憶化(memoization)處理。具體來説,React Compiler 是一個 Babel 插件,這意味着它能夠自動將我們編寫的標準 React 代碼轉化為一種新的形式。在這種形式中,無論是組件內部使用的鈎子(hooks)的依賴關係,還是組件接收的屬性(props),乃至組件本身,都將經過記憶化處理。這種處理方式能顯著優化性能,因為它通過避免不必要的計算和渲染來提高應用的響應速度和效率。
通過這種轉換,原本的 React 代碼將被優化,以確保應用中的數據和組件在不必要更新時能夠保持不變,從而減少性能損耗。這個過程是自動進行的,開發者不需要手動對每個組件或鈎子進行記憶化操作,React Compiler 為我們智能地處理了這一切。
const Component = () => {
const onSubmit = () => {};
const onMount = () => {};
useEffect(() => {
onMount();
}, [onMount]);
return <Form onSubmit={onSubmit} />;
};
就像下面,onSubmit 和 onMount 函數都經過了useCallback的包裹處理,同時 Form 組件也被React.memo包裹
const FormMemo = React.memo(Form);
const Component = () => {
const onSubmit = useCallback(() => {}, []);
const onMount = useCallback(() => {}, []);
useEffect(() => {
onMount();
}, [onMount]);
return <FormMemo onSubmit={onSubmit} />;
};
當然,Compiler 的工作原理並不是直接將代碼轉換成我們上述所描述的形式;實際上,它的操作更為複雜和先進。然而,將這種轉換過程想象為函數和組件通過 useCallback 和 React. memo進行包裹,這樣的思維模型有助於我們更好地理解 Compiler 的作用。
如果你對 Compiler 的具體工作機制感興趣,我推薦你觀看 React 核心團隊成員介紹 Compiler 的視頻。此外,如果你對為何在這些場景中使用 useCallback 和 memo 還有所疑惑,我建議你觀看油管上的《the Advanced React series》 前六集,它們全面講解了組件重新渲染和記憶化的相關知識。如果你更喜歡閲讀,那麼這些文檔內容都是不容錯過的學習資源。
對於 React 教學和學習方法來説,這種技術轉變帶來了一些新的考量。這意味着在理解和應用 React 的過程中,我們需要更新我們的知識庫,同時也提供了一個機會去深入探索 React 的性能優化技巧,讓我們能夠更有效地構建和優化我們的 React 應用。
父子組件重新渲染
目前,如果父組件重新渲染,則裏面所有的子組件也會重新渲染
// 如果 Parent 重新渲染
const Parent = () => {
// Child 也會重新渲染
return <Child />;
};
當前,很多人堅信只有當子組件的屬性(props)發生變化時,組件才會進行重新渲染。我會把這個觀念稱為「重新渲染神話」。實際上,在React的標準行為模式下,這種説法並不成立——屬性的變化並不總是導致組件的重新渲染。
然而,引入 Compiler 後,情況發生了有趣的轉變。得益於 Compiler 在底層實現的全面記憶化處理,這個曾被認為是神話的觀念現在反倒成了 React 的常態。在未來幾年,我們將會向學習者傳授這樣的理念:一個 React 組件僅在其狀態或屬性發生變化時才會重新渲染,而無論其父組件是否進行了重新渲染都不會影響到它。有時,技術的發展確實會帶來一些意想不到的變化和趣味。
不再為了性能而組合
以往,像 “向下移動狀態” 或 “通過子組件傳遞” 這樣的技術被廣泛用於減少不必要的組件重新渲染,以此來提升應用的性能。我建議在嘗試用 useCallback 和 memo 來手動優化之前,可以先考慮這些組合技術,因為在 React 中恰當地實現記憶化(即緩存組件以避免不必要的更新)是一件非常具有挑戰性的事情。
比如,像下面的代碼
const Component = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>
open dialog
</Button>
{isOpen && <ModalDialog />}
<VerySlowComponent />
</>
);
};
由於 ModalDialog 組件是延遲打開的,所以 VerySlowComponent 會在每次 ModalDialog 打開時都重新渲染一次。如果我們把 state 放進 ModalDialog 組件內,像下面這樣
const ButtonWithDialog = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>
open dialog
</Button>
{isOpen && <ModalDialog />}
</>
);
};
const Component = () => {
return (
<>
<ButtonWithDialog />
<VerySlowComponent />
</>
);
};
通過這種方式,我們成功地避免了VerySlowComponent不必要的重新渲染,而且這個過程中我們甚至沒有使用到任何記憶化技術。
隨着 React Compiler 的推出,那些過去為了提升性能而必須採用的編碼模式將不再是必需的。儘管如此,出於代碼組織和關注點分離的考慮,我們可能仍會採用這些模式。但是,過去那種迫使我們將大型組件拆分為更小組件以避免不必要的重新渲染的驅動力將不復存在。在React Compiler的幫助下,我們的組件可以變得更加龐大而不會帶來性能上的負擔。
這表明,React 開發者可以在不犧牲性能的前提下,擁有更大的靈活性來組織和設計他們的組件結構。這一轉變為 React 應用的開發帶來了新的可能性,使得性能優化不再是開發過程中的一個繁重負擔。
不再到處都是 useMemo/useCallback
自然而然,那些有時候讓我們的代碼變得複雜的 useMemo 和 useCallback 將會消失。這部分讓我最為期待。不必再費勁穿梭於多層組件之間,只為了緩存一個 onSubmit 屬性的回調函數。不再有那些難以閲讀和調試的,互相依賴又讓人費解的 useMemo 和 useCallback 鏈條。也不會再因為忘記緩存子組件而導致緩存失效的問題。
差異比對和協調過程
我們或許需要重新思考,如何向人們解釋 React 中的差異比對(diffing)和協調過程(reconciliation)。目前簡化説法是,當我們渲染一個組件,比如 <Child /> 時,實際上我們是在創建它的一個元素對象。這個元素對象大概是下面這樣
{
"type": ...,
"props": ...,
// 其他 react 屬性
}
“type” 是字符串或者組件的引用
在下面的代碼中
const Parent = () => {
return <Child />;
};
當父組件(Parent)發生重渲染時,函數會被觸發,同時<Child />組件的對象會被重新生成。React 會在重渲染之前和之後對這個對象進行一次淺層對比。如果這個對象的引用發生了變化,就意味着 React 需要對這個子組件樹執行一次全面的差異分析。
目前,即便 <Child /> *組件沒有接收任何屬性(props),它還是會不斷地重新渲染,原因就在於<Child />(實際上是對 React.createElement* 函數調用的簡寫)的結果是一個總是被重新創建的對象,因而它無法通過淺層對比的檢查。
隨着 React 編譯器的引入,元素(Elements)、差異對比(diffing)和協調過程(reconciliation)的基本概念仍然保持不變,這是件好事。但變化的是,現在如果<Child ****/>組件的屬性沒有變化,它將返回一個已經被緩存(memoized)的對象。因此,編譯器的引入實際上相當於將所有內容,包括組件元素,都使用了useMemo 進行了包裹和優化。
const Parent = () => {
const child = useMemo(() => <Child />, []);
return child;
};
不過,這只是我基於目前能公開獲取的有限信息所作出的一些推測,所以我的看法可能並不完全準確。不過,這些細節對我們的生產代碼來説並沒有太大的實際影響。
其他方面幾乎和目前的情況一樣。在一個組件內部創建另一個組件,這種做法仍然是一個明顯的設計誤區。我們還是會用 “key” 屬性來識別元素或者重置狀態。處理 Context 仍然是一個棘手的問題。至於數據獲取或錯誤處理等話題,這些目前甚至還沒有被納入討論範圍。
但話説回來,我真的很期待編譯器的發佈。它似乎將為我們的 React 編程帶來重大的改進。
寫在最後
如果你發現這篇博文有幫助,歡迎與其他人分享。 你還可以關注我,瞭解有關 AI、 Javascript、React 和其他 Web 開發主題的更多內容。與往常一樣,如果你有任何疑問,請隨時與我聯繫或發表評論。如果大家遇到任何問題,歡迎 聯繫我 或者直接微信添加 superZidan41。祝你編程愉快!