
如何快速入門VUE3.0:進入學習
在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
undefinedbeing 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)} />
)
})