在 React 專案中,有很多場景需要用到 Ref
。例如使用 ref
屬性獲取 DOM 節點,獲取 ClassComponent 物件範例;用 useRef
Hook 建立一個 Ref 物件,以便解決像 setInterval
獲取不到最新的 state 的問題;你也可以呼叫 React.createRef
方法手動建立一個 Ref
物件。【相關推薦:Redis視訊教學】
雖然 Ref
用起來也很簡單,但在實際專案中實戰還是難免遇到問題,這篇文章將從原始碼的角度出發梳理各種和 Ref
相關的問題,理清和 ref
相關的 API 背後都幹了什麼。看完這篇文章或許可以讓你對的 Ref
有更深入地認識。
首先 ref
是 reference
的簡稱,也就是參照。在 react
的型別宣告檔案中,可以找到好幾個和 Ref 相關的型別,這裡將它們一一列舉出來。
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" ,因為它是唯讀屬性。
檢視 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
。
export function createRef(): RefObject { const refObject = { current: null, }; if (__DEV__) { Object.seal(refObject); } return refObject; }
RefObject/MutableRefObject
是在 16.3
版本才新增的,如果使用更早的版本,需要使用 Ref Callback
。
使用 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
還可以是 字串。
type Ref<T> = RefCallback<T> | RefObject<T> | null; type LegacyRef<T> = string | Ref<T>;
理解了和 Ref 相關的型別,寫起 Typescript 來才能更得心應手。
在 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 原始碼中可以看到,ref
是 RESERVED_PROPS
,同樣有這種待遇的還有 key
,它們都會被特殊處理,從 props 中提取出來傳遞給 Element
。
const RESERVED_PROPS = { key: true, ref: true, __self: true, __source: true, };
所以 ref
是會被特殊處理的 「props「
。
在 16.8.0
版本之前,Function Component 是無狀態的,只會根據傳入的 props render。有了 Hook 之後不僅可以有內部狀態,還可以暴露方法供外部呼叫(需要藉助 forwardRef
和 useImperativeHandle
)。
如果直接對一個 Function Component
用 ref
,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,將它包裹一下定義 $$typeof
為 REACT_FORWARD_REF_TYPE
,return
回去。
跟蹤程式碼,找到 resolveLazyComponentTag,在這裡 $$typeof
會被解析成對應的 WorkTag。
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)。
瞭解如何傳遞 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 執行時的程式碼 ReactFiberHooks
這裡有兩個方法,mountRef
和 updateRef
,顧名思義就是對應 Fiber
節點 mount
和 update
時對 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
,並將它賦值給 hook
的 memoizedState
,update
時直接將它取出返回。
不同的 Hook memoizedState 儲存的內容不一樣,useState
中儲存 state
資訊, useEffect
中 儲存著 effect
物件,useRef
中儲存的是 ref
物件...
mountWorkInProgressHook
,updateWorkInProgressHook
方法背後是一條 Hooks 的連結串列,在不修改連結串列的情況下,每次 render useRef 都能取回同一個 memoizedState 物件,就這麼簡單。
至此,我們瞭解了在 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其它相關文章!