Refs 提供了一種方式,容許咱們訪問 DOM 節點或在 render 方法中建立的 React 元素。 在典型的 React 數據流中,props 是父組件與子組件交互的惟一方式。要修改一個子組件,你須要使用新的 props 來從新渲染它。可是,在某些狀況下,你須要在典型數據流以外強制修改子組件。被修改的子組件多是一個 React 組件的實例,也多是一個 DOM 元素。對於這兩種狀況,React 都提供瞭解決辦法。html
本文基於 React v16.8.6
,本文代碼地址react
相關官方文檔git
class StringRef extends Component {
componentDidMount() {
console.log('this', this);
console.log('this.props', this.props);
console.log('this.refs', this.refs);
}
render() {
return (
<div ref="container"> StringRef </div>
)
}
}
console.log(<StringRef />); 複製代碼
打印的結果github
<StringRef />
是由 React.createlElement
產生的一個對象,自身不是實例,因此它和 this
存在區別。數組
class CallbackRef extends Component {
componentDidMount() {
console.log(this.props);
}
render() {
return (
<div ref={r => this.container = r}> CallbackRef </div>
)
}
}
複製代碼
function ObjectRef(params) {
const r = useRef();
// const r = createRef();
useEffect(() => {
console.log('ObjectRef', r);
});
return (
<div ref={r}> ObjectRef </div>
)
}
複製代碼
class ParentComp extends Component {
componentDidMount() {
setTimeout(() => {
console.log('this.inner', this.inner);
}, 1000);
}
render() {
return (
<ChildComp innerRef={r => this.inner = r} /> ) } } function ChildComp({ innerRef }) { const r = createRef(); useEffect(() => { innerRef(r.current); }); return ( <div ref={r}> ChildComp </div> ) } 複製代碼
這樣從父組件就能夠拿到子組件了。babel
forward refdom
class Input extends Component {
focus = () => {
console.log('focused');
this.input.focus();
}
render() {
return (
<div>
<input ref={r => this.input = r} id="input" />
<button onClick={this.focus}>focus input</button>
</div>
)
}
}
function FocusInput(Comp) {
class FocusInputComp extends React.Component {
render() {
const {forwardedRef, ...rest} = this.props;
// 將自定義的 prop 屬性 「forwardedRef」 定義爲 ref
return <Comp ref={forwardedRef} {...rest} />;
}
}
// 注意 React.forwardRef 回調的第二個參數 「ref」。
// 咱們能夠將其做爲常規 prop 屬性傳遞給 LogProps,例如 「forwardedRef」
// 而後它就能夠被掛載到被 LogPros 包裹的子組件上。
return React.forwardRef((props, ref) => {
return <FocusInputComp {...props} forwardedRef={ref} />;
});
}
function ForwardComp(params) {
const input = useRef();
const ForwardInput = FocusInput(Input);
useEffect(() => {
console.log(input);
setTimeout(() => {
input.current.focus();
}, 1000);
});
return <ForwardInput ref={input} inputName="ForwardInput" />;
}
複製代碼
過 1s 以後輸入框會自動 focus。函數
去除 warning 代碼以後,react/src/forwardRef
中的源碼源碼分析
// 這個 API 我也沒有用過,具體文檔看這裏 https://reactjs.org/docs/forwarding-refs.html
// 總結來講就是能把 ref 傳遞到函數組件上
// 其實沒有這個 API 以前,你也能夠經過 props 的方式傳遞 ref
// 這個實現沒啥好說的,就是讓 render 函數多了 ref 這個參數
export default function forwardRef<Props, ElementType: React$ElementType>( render: (props: Props, ref: React$Ref<ElementType>) => React$Node, ) {
return {
$$typeof: REACT_FORWARD_REF_TYPE,
render,
};
}
複製代碼
這僅僅是構建了一種結構,渲染要交給 react dom。post
打一個 debugger 查看調用棧。
本次調用時 renderWithHooks
的參數。
參數解釋
Component
就是 forwardRef
中的匿名函數props
是 React.forwardRef
生成的組件的 props,傳遞 inputName
時,props 爲 {inputName: xxx}
refOrContext
是 React.forwardRef
生成的組件的 ref在 react-reconciler/src/ReactFiberHooks
中。
// forwardRef 處理的地方
export function renderWithHooks( current: Fiber | null, workInProgress: Fiber, Component: any, props: any, refOrContext: any, nextRenderExpirationTime: ExpirationTime, ): any {
// ...
// forwardRef((props, ref) => (<Comp {...props} forwardRef={ref} />))
// 將從父組件上得到的 props 和 ref 傳遞給匿名函數,這個匿名函數實際也是一個組件(function component)
let children = Component(props, refOrContext);
// ...
return children;
}
複製代碼
再看 updateForwardRef
,在 react-reconciler/src/ReactFiberBeginWork
文件中
這裏顯示的很是清楚。
render
是 React.forwardRef
返回對象的 renderref
就是用 useRef
建立的對象再往上看,調用的是 beginWork
,在 react-reconciler/src/ReactFiberBeginWork
。
在 shared/ReactWorkTags
中 export const ForwardRef = 11;
。再往上不是本篇文章的範圍,不做講解,囧!!!
從上篇 React 源碼文章 React 源碼系列-Component、PureComponent、function Component 分析 ,咱們知道 <StringRef>
由 babel 編譯以後,是由 createElement
來生成一個對象的。函數執行過程當中,會將 ref
屬性從 props
中單獨拿出來。
通過 function createElementWithValidation(type, props, children)
,在 function createElement(type, config, children)
中被提取出來。
if (config != null) {
// 驗證 ref 和 key,只在開發環境下
if (hasValidRef(config)) {
ref = config.ref;
}
// ...
}
複製代碼
在 ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props)
中,ref 傳遞到返回的對象
注意到一個問題沒有?
咱們打印的 <StringRef />
竟然 ref = null
這是由於,咱們的 ref 不在 <StringRef />
的屬性上,而是在 <div />
上!!!
在 ReactElement(建立 element 的最後一個環節)
中打印
結果是
ref => refs
是在哪裏實現的呢?
咱們見到的組件如今都還僅僅是由 createElement
生成的,父與子、root 與 曾曾曾 child 之間的聯繫是 type
,type 是組件自己,class component 是 render 方法裏面返回對象, function component 是直接 return 返回。
咱們生成的結構,是交給 ReactDOM.render
渲染出來的,這篇文章不講渲染部分,直達 refs 。
裏面的 element._owner
就是掛載在 render
方法的class,element 是 render 方法 return 的一部分。
例子
class A extends Component{
render() {
return <StrComp ref="xxx" />; }; } // A 就是 StrComp 的 owner 複製代碼
在 react-reconciler/src/ReactChildFiber
coerceRef
功能: 檢查 element.ref
並返回 ref 函數或者對象
this.refs
function coerceRef( returnFiber: Fiber, current: Fiber | null, element: ReactElement, ) {
// mixedRef 是 function 或者 object 就直接 return 它
let mixedRef = element.ref;
// 若是是字符串,則返回一個函數,這個函數會將 ref 指向的 dom 掛載在 this.refs 上
if (
mixedRef !== null &&
typeof mixedRef !== 'function' &&
typeof mixedRef !== 'object'
) {
// 擁有者 就是給其它組件設置 props 的那個組件。
// 更正式地說,若是組件 Y 在 render() 方法是建立了組件 X,那麼 Y 就擁有 X。
// 組件不能修改自身的 props - 它們老是與它們擁有者設置的保持一致。這是保持用戶界面一致性的基本不變量。
if (element._owner) {
const owner: ?Fiber = (element._owner: any);
let inst; // undefined
// 有 owner 就提取 owner 實例
if (owner) {
const ownerFiber = ((owner: any): Fiber);
// owner 實例(父組件實例)
inst = ownerFiber.stateNode;
}
// mixedRef 強制轉換成字符串
const stringRef = '' + mixedRef;
// Check if previous string ref matches new string ref
if (
current !== null &&
current.ref !== null &&
typeof current.ref === 'function' &&
current.ref._stringRef === stringRef
) {
return current.ref;
}
// 重點是這個函數
const ref = function(value) {
// 拿到 owner stateNode 的 refs
let refs = inst.refs;
// var emptyObject = {};
// {
// Object.freeze(emptyObject);
// }
// Component 中 this.refs = emptyObject;
// export const emptyRefsObject = new React.Component().refs;
if (refs === emptyRefsObject) {
// This is a lazy pooled frozen object, so we need to initialize.
refs = inst.refs = {};
}
if (value === null) {
delete refs[stringRef];
} else {
// 將 dom 和 this.props.refs.xxx 綁定
refs[stringRef] = value;
}
};
// 給 ref 函數添加 _stringRef 屬性爲 stringRef
ref._stringRef = stringRef;
return ref;
}
}
return mixedRef;
}
複製代碼
ownerFiber.stateNode
就是 owner
組件的實例,能夠滑到上面最上面去看 String ref 的 this
咱們打個 debuger 看看。
注意 div...cxqa2
,看看咱們 string ref 指向的 div
ref 函數處理的就是我寫的 string ref。
在 react-reconciler/src/ReactFiberCommitWork
調用棧往上看,調用了 commitAttachRef
。
看到 commitAttachRef
的內容沒有?是它來處理 dom 和 refs、function、createRef object 的掛接的。
function commitAttachRef(finishedWork: Fiber) {
// finishedWork 處理好的 FiberNode, string ref 在這以前被 coerceRef 函數處理好了
const ref = finishedWork.ref;
if (ref !== null) {
// 獲取它的實例
const instance = finishedWork.stateNode;
let instanceToUse;
// 下面的 switch 多是準備加某個功能如今預留出來的
switch (finishedWork.tag) {
// 原生組件,div span ...
case HostComponent:
// function getPublicInstance(instance) {
// return instance;
// }
// instanceToUse === instance true
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
if (typeof ref === 'function') {
// ref 是函數由兩種狀況
// 一、string ref 返回的函數,傳進去 ref 本應該指向的實例,則 `refs[stringRef] = instanceToUse`
// 二、ref 屬性咱們定義了一個函數 `r => this.xxx = r`,則 `this.xxx => instanceToUse`,這樣後面就可使用 `this.xxx` 調用該實例了
ref(instanceToUse);
} else {
// dev 時,檢測對象是否包含 current 屬性
// ...
// 傳進來一個對象,則把實例賦值給 `xx.current`
// `React.createRef()` 返回一個對象 `{current: null}`
// `React.useRef()` 返回一個對象 `{current: undefined}`
// 給變量引用的對象的某個屬性賦值,在其餘做用域依然能夠獲取到該屬性
ref.current = instanceToUse;
}
}
}
複製代碼
finishedWork
這裏 finishedWork.stateNode
就是 html div
forwardRef
renderWithHooks
updateForwardRef
beginWork
coerceRef
commitAttachRef
關於 ref 的源碼分析就到這裏。收穫不少,這篇文章大大加深了我對 ref 的理解!!!
沒作源碼分析以前,總感受很是困難, react-dom 源碼就有 2.5w 行,看到了都怕!!!如今越分析越有勁,每次分析都是在不斷加深理解。
廣告時間
歡迎關注,每日進步!!!