帶你瞭解React中的Ref,值得了解的知識點分享

2022-03-22 13:00:31
本篇文章帶大家瞭解一下React中的Ref,介紹一些關於 Ref 你需要知道的知識點,希望對大家有所幫助!

Intro

在 React 專案中,有很多場景需要用到 Ref。例如使用 ref 屬性獲取 DOM 節點,獲取 ClassComponent 物件範例;用 useRef Hook 建立一個 Ref 物件,以便解決像 setInterval 獲取不到最新的 state 的問題;你也可以呼叫 React.createRef 方法手動建立一個 Ref 物件。【相關推薦:Redis視訊教學

雖然 Ref 用起來也很簡單,但在實際專案中實戰還是難免遇到問題,這篇文章將從原始碼的角度出發梳理各種和 Ref 相關的問題,理清和 ref 相關的 API 背後都幹了什麼。看完這篇文章或許可以讓你對的 Ref 有更深入地認識。

Ref 相關的型別宣告

首先 refreference 的簡稱,也就是參照。在 react 的型別宣告檔案中,可以找到好幾個和 Ref 相關的型別,這裡將它們一一列舉出來。

RefObject/MutableRefObject

interface RefObject<T> { readonly current: T | null; }
interface MutableRefObject<T> { current: T; }

使用 useRef Hook 的時候返回的就是 RefObject/MutableRefObejct,這兩個型別都是定義了一個 { current: T } 的物件結構,區別是 RefObject 的 current 屬性是唯讀的,如果修改 refObject.current,Typescript 會警告⚠️。

const ref = useRef<string>(null)
ref.current = '' // Error

TS 報錯:無法分配到 "current" ,因為它是唯讀屬性。

1.png

檢視 useRef 方法的定義,這裡用了函數過載,當傳入的泛型引數 T 不包含 null 時返回RefObject<T>,當包含 null 時將返回 MutableRefObject<T>

function useRef<T>(initialValue: T): MutableRefObject<T>;
function useRef<T>(initialValue: T | null): RefObject<T>;

所以如果你希望建立的 ref 物件 current 屬性是可修改的,需要加上 | null

const ref = useRef<string | null>(null)
ref.current = '' // OK

呼叫 React.createRef() 方法時返回的也是一個 RefObject

createRef

export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  if (__DEV__) {
    Object.seal(refObject);
  }
  return refObject;
}

RefObject/MutableRefObject 是在 16.3 版本才新增的,如果使用更早的版本,需要使用 Ref Callback

RefCallback

使用 Ref Callback 就是傳遞一個回撥函數,react 回撥時會將對應的範例回傳過來,可以自行儲存以便呼叫。這個回撥函數的型別就是 RefCallback

type RefCallback<T> = (instance: T | null) => void;

使用 RefCallback 範例:

import React from 'react'

export class CustomTextInput extends React.Component {
  textInput: HTMLInputElement | null = null;

  saveInputRef = (element: HTMLInputElement | null) => {
    this.textInput = element;
  }

  render() {
    return (
      <input type="text" ref={this.saveInputRef} />
    );
  }
}

Ref/LegacyRef

在型別宣告中,還有 Ref/LegacyRef 型別,它們用於泛指 Ref 型別。 LegacyRef 是相容版本,在之前的老版本 ref 還可以是 字串

type Ref<T> = RefCallback<T> | RefObject<T> | null;
type LegacyRef<T> = string | Ref<T>;

理解了和 Ref 相關的型別,寫起 Typescript 來才能更得心應手。

Ref 的傳遞

特殊的 props

在 JSX 元件上使用 ref 時,我們是通過給 ref 屬性設定一個 Ref。我們都知道 jsx 的語法,會被 Babel 等工具編譯成 createElement 的形式。

// jsx
<App ref={ref} id="my-app" ></App>

// compiled to
React.createElement(App, {
  ref: ref,
  id: "my-app"
});

看起來 ref 和其他 prop 沒啥區別,不過如果你嘗試在元件內部列印 props.ref 卻是 undefined。並且 dev 環境控制檯會給出提示。

Trying to access it will result in undefined being returned. If you need to access the same value within the child component, you should pass it as a different prop.

React 對 ref 做了啥?在 ReactElement 原始碼中可以看到,refRESERVED_PROPS,同樣有這種待遇的還有 key,它們都會被特殊處理,從 props 中提取出來傳遞給 Element

const RESERVED_PROPS = {
  key: true,
  ref: true,
  __self: true,
  __source: true,
};

所以 ref 是會被特殊處理的 「props「

forwardRef

16.8.0 版本之前,Function Component 是無狀態的,只會根據傳入的 props render。有了 Hook 之後不僅可以有內部狀態,還可以暴露方法供外部呼叫(需要藉助 forwardRefuseImperativeHandle)。

如果直接對一個 Function Componentref,dev 環境下控制檯會告警,提示你需要用 forwardRef 進行包裹起來。

function Input () {
    return <input />
}

const ref = useRef()
<Input ref={ref} />

Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

forwardRef 為何物?檢視原始碼 ReactForwardRef.js__DEV__ 相關的程式碼摺疊起來,它只是一個無比簡單的高階元件。接收一個 render 的 FunctionComponent,將它包裹一下定義 $$typeofREACT_FORWARD_REF_TYPEreturn 回去。

2.png

跟蹤程式碼,找到 resolveLazyComponentTag,在這裡 $$typeof 會被解析成對應的 WorkTag。

3.png

REACT_FORWARD_REF_TYPE 對應的 WorkTag 是 ForwardRef。緊接著 ForwardRef 又會進入 updateForwardRef 的邏輯。

case ForwardRef: {
  child = updateForwardRef(
    null,
    workInProgress,
    Component,
    resolvedProps,
    renderLanes,
  );
  return child;
}

這個方法又會呼叫 renderWithHooks 方法,並在第五個引數傳入 ref

nextChildren = renderWithHooks(
  current,
  workInProgress,
  render,
  nextProps,
  ref, // 這裡
  renderLanes,
);

繼續跟蹤程式碼,進入 renderWithHooks 方法,可以看到,ref 會作為 Component 的第二個引數傳遞。到這裡我們可以理解被 forwardRef 包裹的 FuncitonComponent 第二個引數 ref 是從哪裡來的(對比 ClassComponent contructor 第二個引數是 Context)。

4.png

瞭解如何傳遞 ref,那下一個問題就是 ref 是如何被賦值的。

ref 的賦值

打斷點(給 ref 賦值一個 RefCallback,在 callback 裡面打斷點) 跟蹤到程式碼 commitAttachRef,在這個方法裡面,會判斷 Fiber 節點的 ref 是 function 還是 RefObject,依據型別處理 instance。如果這個 Fiber 節點是 HostComponent (tag = 5) 也就是 DOM 節點,instance 就是該 DOM 節點;而如果該 Fiber 節點是 ClassComponent (tag = 1),instance 就是該物件範例。

function commitAttachRef(finishedWork) {
  var ref = finishedWork.ref;

  if (ref !== null) {
    var instanceToUse = finishedWork.stateNode;

    if (typeof ref === 'function') {
      ref(instanceToUse);
    } else {
      ref.current = instanceToUse;
    }
  }
}

以上是 HostComponent 和 ClassComponent 中對 ref 的賦值邏輯,對於 ForwardRef 型別的元件走的是另外的程式碼,但行為基本是一致的,可以看這裡 imperativeHandleEffect

接下里,我們繼續挖掘 React 原始碼,看看 useRef 是如何實現的。

useRef 的內部實現

通過跟蹤程式碼,定位到 useRef 執行時的程式碼 ReactFiberHooks

5.png

這裡有兩個方法,mountRefupdateRef,顧名思義就是對應 Fiber 節點 mountupdate 時對 ref 的操作。

function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

function mountRef<T>(initialValue: T): {|current: T|} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

可以看到 mount 時,useRef 建立了一個 RefObject,並將它賦值給 hookmemoizedStateupdate 時直接將它取出返回。

不同的 Hook memoizedState 儲存的內容不一樣,useState 中儲存 state 資訊, useEffect 中 儲存著 effect 物件,useRef 中儲存的是 ref 物件...

mountWorkInProgressHookupdateWorkInProgressHook 方法背後是一條 Hooks 的連結串列,在不修改連結串列的情況下,每次 render useRef 都能取回同一個 memoizedState 物件,就這麼簡單。

應用:合併 ref

至此,我們瞭解了在 React 中 ref 的傳遞和賦值邏輯,以及 useRef 相關的原始碼。用一個應用題來鞏固以上知識點:有一個 Input 元件,在元件內部需要通過 innerRef HTMLInputElement 來存取 DOM 節點,同時也允許元件外部 ref 該節點,需要怎麼實現?

const Input = forwardRef((props, ref) => {
  const innerRef = useRef<HTMLInputElement>(null)
  return (
    <input {...props} ref={???} />
  )
})

考慮一下上面程式碼中的 ??? 應該怎麼寫。

============ 答案分割線 ==============

通過了解 Ref 相關的內部實現,很明顯我們這裡可以建立一個 RefCallback,在裡面對多個 ref 進行賦值就可以了。

export function combineRefs<T = any>(
  refs: Array<MutableRefObject<T | null> | RefCallback<T>>
): React.RefCallback<T> {
  return value => {
    refs.forEach(ref => {
      if (typeof ref === 'function') {
        ref(value);
      } else if (ref !== null) {
        ref.current = value;
      }
    });
  };
}

const Input = forwardRef((props, ref) => {
  const innerRef = useRef<HTMLInputElement>(null)
  return (
    <input {...props} ref={combineRefs(ref, innerRef)} />
  )
})

更多程式設計相關知識,請存取:!!

以上就是帶你瞭解React中的Ref,值得了解的知識點分享的詳細內容,更多請關注TW511.COM其它相關文章!