您好,登錄后才能下訂單哦!
這篇文章主要介紹React中的Ref是什么,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
在 React 項目中,有很多場景需要用到 Ref
。例如使用 ref
屬性獲取 DOM 節點,獲取 ClassComponent 對象實例;用 useRef
Hook 創建一個 Ref 對象,以便解決像 setInterval
獲取不到最新的 state 的問題;你也可以調用 React.createRef
方法手動創建一個 Ref
對象。
雖然 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
。
createRef
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是什么”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。