日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

React?Ref?Callback使用場景最佳實踐詳解_React

作者:KooFE ? 更新時間: 2023-03-01 編程語言

引言

本文源于翻譯?React ref Callback Use Cases

在 React 中,"Ref" 具有兩個相關的含義,而且經常讓人困惑。在本文正式開始之前,我們先弄清楚它的定義:

  • 作為 useRef hook 返回的 “ref 對象”:在這種場景中,它就是一個普通的 JavaScript 對象,具有一個名為 current 的屬性,并且可以讀取或設置為任意值。
  • 作為 JSX DOM 元素上的 “ref 屬性”:用于訪問其對應的 DOM 元素

這兩者經常一起使用,“ref 對象” 可以傳遞給 “ref 屬性”,React 會將對 DOM 元素設置為它的 current 屬性值。

ref callback

ref 屬性除了接受 ref 對象之外,還可以接受函數也就是 ref callback。在該函數中,DOM 元素作為其唯一參數。

與 effect 函數一樣,React 在組件周期中的某些時刻中調用它。當創建 DOM 元素之后會立即執行 ref callback(參數是 DOM 元素),在刪除元素時也會再次調用 ref callback,只不過這時的參數是 null。

如果 ref callback 被定義為內聯函數,React 將在每次渲染時調用它兩次,第一次的參數是 null,第二次的參數是 DOM 元素。

雖然內聯 ref callback 被調用兩次可能會令人驚訝,如果從 React 的角度來看,我認為這種行為是合理的。它保證了一致性,因為每次渲染都會創建新的函數實例,它可能是一個完全不同的函數。這些函數可能會依賴 props 或 state,而這些 props 或 state 也可能在此期間發生了變化。

因此 React 需要清除舊的 ref callback(參數是 null),然后設置新的回調(參數是 DOM 元素)。這樣我們可以根據條件來設置 ref 屬性的值,甚至在 React 元素之間交換它們。

這可能會導致一些不必要的調用。在大多數情況下,這不是引起什么問題。如果你不想執行這些不必要的調用,可以通過在 useCallback 中包裝 ref callback或將函數移出組件來避免這種行為。

使用場景

在 React Docs 中關于 ref callback 的內容較少。也許是他們故意不去討論它,因為它的使用場景非常少,訪問 DOM 元素的場景并不多見。

ref callback 是 React 的一個小眾功能,你不會每天都需要它。盡管如此,還是有一些場景會用到它,否則,它就不會存在于 React 中了!所以讓我們來看一下在哪些場景會用到它。

需要明確的是,只有在訪問底層的 DOM 元素時,才需要 ref callback。那么,ref callback 何時有用?答案是,當您想要在 React 中對 DOM 元素執行操作時,請使用 ref callback。據我所知,這可以歸結為以下四種情況。

  • 當元素掛載或更新時,調用 DOM 元素上的方法來執行一些操作。
  • “通知” DOM 元素的更改,當 DOM 元素的某些屬性發生更改時,重新讀取該元素。
  • 將 DOM 元素設置到 state 中,以便在渲染期間訪問它。
  • 共享 DOM 元素;使用 DOM 元素執行多項操作。

現在,讓我們具體來看看每一個場景。

1. DOM 元素掛載并滾動到它所在的位置

您可以在 ref callback 中調用 DOM 元素上的方法,以執行滾動或聚焦等 DOM 操作。例如,自動滾動到列表中的最后一項:

// On first render and on unmount there 
// is no DOM element so `el` will be `null` 
const scrollTo = (el) => {
  if (el) {
    el.scrollIntoView({ behavior: "smooth" });
  }
};
function List({ data }) {
  return (
    <ul>
      {data.map((d, i) => {
        const isLast = i === data.length - 1;
        return (
          <li
            key={d.name}
            // ref callback to scroll to the last list element
            ref={isLast ? scrollTo : undefined}
          >
            {d.name}
          </li>
        );
      })}
    </ul>
  );
}

記住,管理 DOM 是 React 的工作,避免執行 DOM 上的可變方法, 比如(insert, remove, set, replace 等),對于 focus 和 scroll 等非破壞性的操作則允許我們開發實現。

還要注意,瀏覽器在沒有用戶交互的情況下是不允許調用 DOM 元素的某些方法,如 requestFullscreen。當在 ref callback 中調用時,所有此類受保護的方法都不會執行任何操作。

2. 當 DOM 元素變化時的重新渲染

當我們通過 React 訪問某些 DOM 元素屬性時,可以使用 ref callback。當我們讀取一個 DOM 元素屬性,比如滾動位置,或者在 ref callback 中調用一個獲取元素信息的方法,比如 getBoundingClientRect(),并將該信息設置到 state 中。

測量 DOM 元素

這是一段直接來自(舊)React 文檔的片段:如何測量 DOM 節點。這是 ref callback的一個很好的例子,所以將其復制到這里。

const [size, setSize] = useState();
const measureRef = useCallback((node) => {
  setSize(node.getBoundingClientRect());
}, []);
return <div ref={measureRef}>{children}</div>;

在這個案例中,沒有選擇使用 useRef,因為當 ref 是一個對象時,它并不會把當前 ref 值的變化情況通知到我們。使用 callback ref 可以確保即便被測量的節點在子組件延遲顯示 (比如為了響應一次點擊),我們依然能夠在父組件接收到相關的信息,以便更新測量結果。注意到我們傳遞了 [] 作為 useCallback 的依賴列表。這確保了 ref callback 不會在再次渲染時改變,因此 React 不會在非必要的時候調用它。

3. 在 render 中訪問 DOM 元素

如果在 ref 回調中將 DOM 元素設置到 state,它將觸發新的渲染,因為這正是設置 state 的作用。但是它不會陷入無限渲染循環,因為 setState 是一個穩定的函數,因此 ref callback 僅在掛載和卸載時調用。

在這種情況下,為什么我們不使用 useRef?答案是,因為不允許在渲染過程中訪問 ref 對象。對于渲染中的 DOM 元素,必須通過 state 來訪問。接下來舉幾個例子進行介紹。

React Portal

React portal 主要用于解決組件樹和 DOM 樹的結構之間不一致的問題。portal 將 DOM 樹上不同位置上的組件連接到一起,最為常使用的場景就是將 Modal 彈窗覆蓋整個視窗。

// Assume an empty div with id 'modal' is in your HTML
const modalEl = document.getElementById("modal");
function Modal({ children, ...props }) {
  return ReactDOM.createPortal(
    <ModalBase {...props}>
      {children}
    </ModalBase>,
    modalEl
  );
}

可以使用 document.getElementById() 來獲取 DOM 元素,前提是你能保證它是存在的。或許你不想通過 HTML 來控制 Modal,而是希望能 portal 到一個 React 創建的 DOM 元素上。

這就需要在進行 render 時訪問到相應的 DOM 元素,使用 ref callback 可以實現這個功能。

function Parent() {
  const [modalElement, setModalElement] = useState(null);
  return (
    <div>
      <div id="modal-location" ref={setModalElement} />
      {/* Imagine that the modal container and the
          Modal itself are farther apart in the component tree */}
      <Modal modalElement={modalElement}>Warning</Modal>
    </div>
  )
}
function Modal({ children, modalElement, ...props }) {
  return modalElement
    ? ReactDOM.createPortal(
        <ModalBase {...props}>{children}</ModalBase>,
        modalElement
      )
    : null;

在最開始,modalElement 的值是 null,所以需要在創建 portal 之前做一下判斷。

非受控復合組件

非受控復合組件(Uncontrolled Compound Components)是一種高級的 React 模式,其核心是 ref callback 來處理 React portal。

復合組件(Compound Component)是將多個組件組合到一起工作,進而形成一個能夠展示的 UI。復合組件將復雜的功能拆分為更小的塊,并且它們在一起共同完成整個復雜功能。這樣就可以避免產生一個有很多 props 的“上帝組件”。

這種模式與 HTML 元素組合比較類似,比如:

  • <select> 中包含多個 <option>
  • <table> 組件會由 <thead>和 <tbody> 組成
  • <details> 元素中會包含 <summary>

在 React 中,數據總是向下流動。當數據流不符合組件樹的結構時,我們可以通過提升 state 來調整數據流。大多數時候,這是一個很好的解決方案。

對于一些共享組件,如對話框或側邊欄,頁面上只能有一個,提升 state 會使這些 state “過于全局”。這些 state 將通過許多中間組件連接起來,而這些中間組件實際上并不需要知道它。它會污染整個鏈條上的組件,并會使代碼變得混亂。

我們可以把 React state 看作是懸掛在組件樹上的繩子。繩子的長度代表了定義 state 的組件到使用 state 的 UI 之間的距離。所以,當你的繩子長度越長、數量越多時,它們就越容易被纏在一起。所以要盡量縮短繩子的長度,同時控制繩子的數量。

出現這個問題的根源是,我們強行將組件樹和數據流適配成 DOM 樹的形狀。反過來,我們可以通過組件樹適應數據流來反轉控制。使用 Portal,我們可以重置組件間的距離,并保持 DOM 結構不變。這樣就可以將拉近相關組件的距離,即便是在 DOM 樹中的離得很遠。這使我們的 React 代碼更容易理解。

非受控復合組件的實現過程:

  • ref callback定位到元素位置。獲取 DOM 元素并將其置于 state 中。這里我們不能使用 ref 對象,因為我們需要在 render 中使用它,而且要在設置它的時候觸發更新。
  • 用 Context 共享元素位置。將這個 DOM 元素放存放在 context 中,以便 context 中的所有組件都可以訪問 DOM 中的這個位置。
  • 使用 Portal 連接到該位置。從 context 中獲取對 DOM元素,并將組件進行 portal。

如果你在想 “這一切聽起來很復雜”,是的,你沒有錯。這是一種高級模式,需要一些額外的成本,作為回報 -- 它能夠讓你編寫了更簡單的組件。這是值得的(IMO)。也許你會發現,你并不經常需要它,但是一旦有需要時,就會體驗到其中的樂趣。“Amazing”!

比如在下面的例子中實現一個復雜的面包屑:

每個 <Breadcrumb/> 都會 portal 到 <BreadcrumbPortal/> 中的 breadcrumbElement 元素

<BreadcrumbPortal/> 會按它們的渲染順序在 <Breadcrumbs/> 展示出來

如果 <BreadcrumbPortal/> 沒有渲染,這時 breadcrumbElement 為 null,<Breadcrumb/> 也不會渲染

4. 共享 DOM Ref

經常會出現不止一個消費者需要訪問 DOM 元素。假設你想測量一個 <div>的寬度,并將其交給另一個 React 之外的庫來處理 DOM 內容。對于 React 來說,這樣的元素就是一個黑盒。React 既不知道它的內部有什么,也不關心它是什么。這個元素完全交給另外一個庫來管理。

一個典型的例子就是使用 D3 或 @observable/plot 創建的響應式圖標。在下面的例子中,我們會使用 @observable/plot 創建一個 plot,并且使用 react-use-measure 來計算元素的寬度。使用 ref callback將 DOM 元素傳遞給它們倆:

import useMeasure from "react-use-measure";
import * as Plot from "@observablehq/plot";
export function BoxPlot({ data }) {
  const [measureRef, { width, height }] = useMeasure({ debounce: 5 });
  const plotRef = useRef<HTMLDivElement | null>(null);
  useEffect(() => {
    const boxPlot = Plot.plot({
      width: Math.max(150, width),
      marks: [
        Plot.boxX(data),
      ],
    });
    plotRef.current.append(boxPlot);
    return () => boxPlot.remove();
  }, [data, width]);
  const initBoxPlot = useCallback((el: HTMLDivElement | null) => {
    plotRef.current = el;
    measureRef(el);
  }, []);
  return <div ref={initBoxPlot} />;
}

總結

  • ref callback是一個傳遞給元素的 ref 屬性的函數。React 會在組件掛載時調用它,這時的參數是 DOM 元素;當組件卸載的時候也會調用它,這時的參數是 null。
  • 當你設置不同的 ref callback時,React 也會調用 ref callback
  • 切換綁定和取消綁定 ref 到一個 DOM 元素,ref callback可以讓我們實現特定的操作。

ref callback可以用來做以下事情

  • 操作 DOM,比如在組件掛載的時候滾動或聚焦
  • 在 React 獲取 DOM 屬性,比如寬度或滾動位置
  • 在 React 控制的 DOM 元素上使用 Portal

非受控復合組件是一個很強大的模式,使用了 Portal

將 DOM 元素提供給多個消費者

原文鏈接:https://juejin.cn/post/7187429820928622629

欄目分類
最近更新