動態

詳情 返回 返回

[譯] 如何使用 useRef 修復 React 性能問題 - 動態 詳情

  • 原文地址:How to useRef to Fix React Performance Issues
  • 原文作者:Sidney Alcantara
  • 譯文出自:掘金翻譯計劃
  • 本文永久鏈接:https://github.com/xitu/gold-miner/blob/master/article/2020/how-to-useref-to-fix-react-performance-issues.md
  • 譯者:NieZhuZhu「彈鐵蛋同學」
  • 校對者:regon-cao、zenblo

如何使用 useRef 修復 React 性能問題

Photo by the author.

Refs 是 React 中很少會使用到的特性。如果你已經讀過了官方的 React Ref Guide,你會從中瞭解到 Refs 被描述為重要的 React 數據流的 “逃生艙門”,需謹慎使用。Refs 被視為訪問組件的基礎 DOM 元素的正確方法。

伴隨着 React Hooks 的到來,React 團隊引入了 useRef Hook,它擴展了這個功能:

useRef() 比 ref 屬性更有用。它通過類似在 class 中使用實例字段的方式,非常方便地 保存任何可變值。” —— React 文檔

新的 React Hooks API 發佈的時候,我的確忽略了這一點,事實證明 useRef 真的非常有用。

面臨的問題

我是一名 Firetable 的軟件開發工程師。Firetable 是一個開源的 React 電子表格應用,結合了 Firestore 和 Firebase 的主要功能。其中有一個主要功能是側面抽屜,它是一種類似於窗體的 UI,用於編輯在主表上滑動的那一行。

image

當用户單擊選中表格中的某一個單元格時,可以通過打開側抽屜的方式編輯該單元格所對應的行數據。 換句話説,我們在側邊抽屜中渲染的內容取決於當前選擇的行 —— 我們需要將這行的數據狀態記錄下來。

將這行數據的狀態的放在側抽屜組件內部是最符合邏輯的,因為當用户選擇其他單元格時,它應該影響側邊的抽屜組件。 然而:

  • 我們需要在表格組件裏設置這個數據狀態。我們用的是 react-data-grid 渲染表格,並且它接收一個當用户點擊一個單元格時會觸發的回調。就目前來看,這是我們能從表格中獲取選中行數據的唯一途徑。
  • 但是側邊抽屜組件和表格組件是同級(兄弟)組件,所以不能直接訪問彼此的數據狀態。

React 的推薦做法是 提升狀態 到倆組件最近的父級節點 (以這個為例,父級節點為 TablePage)。但是我們決定不將狀態遷移到這個組件,理由是:

  1. TablePage 不保存狀態,主要是放置 table 和 side drawer 組件的容器, 兩者都不接收任何的 props。我們傾向於保持這種做法。
  2. 我們已經在組件樹的頂層使用 React Context 來共享了許多的全局數據,並且我們覺得應該將這個狀態上升到全局 store。

注意:即使我們將數據狀態放在了 TablePage,無論如何我們都將面臨下面這個相同的問題。

問題就是每當用户選擇一個單元格或打開側面抽屜時,全局 context 的更新會使得整個應用發生重新渲染。table 組件可以一次顯示數十個單元格,並且每個單元格都有自己的編輯器組件。這會導致大約 650ms 的渲染時間,這個時間太長以至於在打開側邊抽屜的時候會感受到明顯的延遲。

罪魁禍首是 context —— 這就是為什麼要在 React 中使用而不是在全局 JavaScript 對象中使用:

”只要提供給 Provider 的值發生變化,所有消費到了 Provider 的後代組件都會發生重渲染。“ — React Context

到目前為止,雖然我們已經足夠了解 React 的狀態和生命週期,但現在看來我們依舊陷入了困境。

頓悟時刻!

image

在決定使用 useRef 之前,我們嘗試了幾種不同的解決方案。(Dan Abramov 的文章) :

  1. 拆分 context (也就是創建新的 SideDrawerContext) —— table 組件仍然會消費到新的 context,在打開側邊抽屜的時候依舊會 導致 table 組件的不必要的重新渲染。
  2. 將 table 組件放在 React.memouseMemo 中 —— table 組件依舊是需要通過 useContext 拿到側邊抽屜組件的狀態,兩種 API 均無法阻止其重新渲染。
  3. 將用於渲染表格的 react-data-grid 組件進行 memo —— 這將使我們的代碼更加的冗長。我們還發現它阻止了 “必要” 的重新渲染,要求我們花費更多的時間完全修復或者重構我們的代碼來實現側邊抽屜。

當再次閲讀 Hook APIs 和 useMemo 文檔的時候,我終於遇到了 useRef 相關內容。

useRef() 比 ref 屬性更有用。它通過像在 class 中使用實例字段的方式,非常方便地 保存任何可變值。” —— React 文檔

更重要的是:

“當 ref 對象內容發生變化時,useRef 並不會通知變更。變更 .current 屬性不會引發組件重新渲染。” —— React 文檔

此時:我們不需要存儲側抽屜的狀態。我們只需要引用設置該狀態的函數即可。

解決方案

  1. 將打開狀態和單元狀態保存在側面抽屜組件中。
  2. 創建這些狀態的 ref,並將其存儲在 context 中。
  3. 當用户單擊單元格時,使用之前説的表中的回調去調用 ref 設置數據狀態的函數(在側抽屜內)。

以下代碼是在 Firetable 使用的代碼縮寫版,其中包括了 ref 和 TypeScript 的類型:

import { SideDrawerRef } from 'SideDrawer'

export function FiretableContextProvider({ children }) {
  const sideDrawerRef = useRef<SideDrawerRef>();

  return (
    <FiretableContext.Provider value={{ sideDrawerRef }}>
      {children}
    </FiretableContext.Provider>
  )
}

注意:由於函數組件在重新渲染時會運行整個函數體,所以每當 “單元” 或 “打開” 狀態更新(並導致重新渲染)時,“sideDrawerRef” 總是能在 “.current” 中獲取到最新值。

事實證明,此解決方案是最佳的:

  1. 當前的單元格和打開的狀態存儲在側面抽屜組件中 —— 這是放置它的最合邏輯的地方。
  2. 當需要時,表格組件也可以訪問其兄弟組件的狀態。
  3. 當前單元格或打開狀態更新時,它只會觸發側抽屜組件的重新渲染,而不觸發整個應用程序中的其他組件重新渲染。

你可以在 Firetable 源碼中看它是如何被使用的 GitHub.

什麼時候使用 useRef

不過,這並不意味着您可以在應用中隨意使用。當您需要在特定時間訪問或更新另一個組件的狀態,但是您的其他組件不依賴於該狀態或基於該狀態進行呈現時,這是最好的辦法。 React 的提升狀態和單向數據流的核心概念足以覆蓋大多數應用程序架構。

🌈 今天的文章分享就到這裏啦,如果喜歡這篇文章的話請點贊、Star、關注我吧 🎯
image.png

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久鏈接 即為本文在 GitHub 上的 MarkDown 鏈接。

掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。

Add a new 評論

Some HTML is okay.