React Ref 其實是這樣的
大家好,我是Mokou,好久沒有冒泡了,最近一直在看研究演算法和數據結構方面的東西,但是似乎很多前端不喜歡看這種東西,而且目前本人演算法方面也很挫,就不獻醜了。
當然了,最近也開始研究React了,這篇文章主要是講述 Ref 相關的內容,如有錯誤請指正。
ref 的由來
在典型的 React 數據流中,props 是父組件與子組件交互的唯一方式。要修改一個子組件,你需要使用新的 props 來重新渲染它。但是,在某些情況下,你需要在典型數據流之外強制修改子組件/元素。
適合使用 refs 的情況:
- 管理焦點,文本選擇或媒體播放。
- 觸發強制動畫。
- 集成第三方 DOM 庫。
ref 的三種方式
在 React v16.3 之前,ref 通過字元串(string ref)或者回調函數(callback ref)的形式進行獲取。
ref 通過字元獲取:
// string ref
class MyComponent extends React.Component {
componentDidMount() {
this.refs.myRef.focus();
}
render() {
return <input ref="myRef" />;
}
}
ref 通過回調函數獲取:
// callback ref
class MyComponent extends React.Component {
componentDidMount() {
this.myRef.focus();
}
render() {
return <input ref={(ele) => {
this.myRef = ele;
}} />;
}
}
在 v16.3 中,經 0017-new-create-ref 提案引入了新的 API:React.createRef
。
ref 通過 React.createRef 獲取:
// React.createRef
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
componentDidMount() {
this.myRef.current.focus();
}
render() {
return <input ref={this.myRef} />;
}
}
將被移除的 string ref
首先來具體說說 string ref
,string ref 就已被詬病已久,React 官方文檔中如此聲明:"如果你目前還在使用 this.refs.textInput 這種方式訪問 refs ,我們建議用回調函數或 createRef API 的方式代替。"
,為何如此糟糕?
最初由 React 作者之一的 dan abramov
。發佈於//news.ycombinator.com/edit?id=12093234,(該網站需要梯子)。吐槽內容主要有以下幾點:
- string ref 不可組合。 例如一個第三方庫的父組件已經給子組件傳遞了 ref,那麼我們就無法在在子組件上添加 ref 了。 另一方面,回調引用沒有一個所有者,因此您可以隨時編寫它們。例如:
/** string ref **/
class Parent extends React.Component {
componentDidMount() {
// 可獲取到 this.refs.childRef
console.log(this.refs);
}
render() {
const { children } = this.props;
return React.cloneElement(children, {
ref: 'childRef',
});
}
}
class App extends React.Component {
componentDidMount() {
// this.refs.child 無法獲取到
console.log(this.refs);
}
render() {
return (
<Parent>
<Child ref="child" />
</Parent>
);
}
}
- string ref 的所有者由當前執行的組件確定。 這意味著使用通用的「渲染回調」模式(例如react),錯誤的組件將擁有引用(它將最終在react上而不是您的組件定義renderRow)。
class MyComponent extends Component {
renderRow = (index) => {
// string ref 會掛載在 DataTable this 上
return <input ref={'input-' + index} />;
// callback ref 會掛載在 MyComponent this 上
return <input ref={input => this['input-' + index] = input} />;
}
render() {
return <DataTable data={this.props.data} renderRow={this.renderRow} />
}
}
- string ref 不適用於Flow之類的靜態分析。 Flow不能猜測框架可以使字元串ref「出現」在react上的神奇效果,以及它的類型(可能有所不同)。 回調引用比靜態分析更友好。
- string ref 強制React跟蹤當前正在執行的組件。 這是有問題的,因為它使react模組處於有狀態,並在捆綁中複製react模組時導致奇怪的錯誤。在 reconciliation 階段,React Element 創建和更新的過程中,ref 會被封裝為一個閉包函數,等待 commit 階段被執行,這會對 React 的性能產生一些影響。
關於這點可以參考 React 源碼 coerceRef
的實現:
在調和子節點得過程中,會對 string ref 進行處理,把他轉換成一個方法,這個方法主要做的事情就是設置 instance.refs[stringRef] = element,相當於把他轉換成了function ref
對於更新得過程中string ref是否變化需要對比得是 current.ref._stringRef,這裡記錄了上一次渲染得時候如果使用得是string ref他的值是什麼
owner是在調用createElement的時候獲取的,通過ReactCurrentOwner.current獲取,這個值在更新一個組件前會被設置,比如更新ClassComponent的時候,調用render方法之前會設置,然後調用render的時候就可以獲取對應的owner了。
堅挺的 callback ref
React 將在組件掛載時,會調用 ref 回調函數並傳入 DOM 元素,當卸載時調用它並傳入 null。在 componentDidMount 或 componentDidUpdate 觸發前,React 會保證 refs 一定是最新的。
如果 ref 回調函數是以內聯函數的方式定義的,在更新過程中它會被執行兩次,第一次傳入參數 null,然後第二次會傳入參數 DOM 元素。這是因為在每次渲染時會創建一個新的函數實例,所以 React 清空舊的 ref 並且設置新的。通過將 ref 的回調函數定義成 class 的綁定函數的方式可以避免上述問題,但是大多數情況下它是無關緊要的。
最新的 React.createRef
React.createRef 的優點:
- 相對於 callback ref 而言 React.createRef 顯得更加直觀,避免了 callback ref 的一些理解問題。
React.createRef 的缺點:
- 性能略低於 callback ref
- 能力上仍遜色於 callback ref,例如上一節提到的組合問題,createRef 也是無能為力的。
ref 的值根據節點的類型而有所不同:
- 當 ref 屬性用於 HTML 元素時,構造函數中使用 React.createRef() 創建的 ref 接收底層 DOM 元素作為其 current 屬性。
- 當 ref 屬性用於自定義 class 組件時,ref 對象接收組件的掛載實例作為其 current 屬性。
- 默認情況下,你不能在函數組件上使用 ref 屬性(可以在函數組件內部使用),因為它們沒有實例:
- 如果要在函數組件中使用 ref,你可以使用 forwardRef(可與 useImperativeHandle 結合使用)
- 或者可以將該組件轉化為 class 組件。
Refs 轉發
是否需要將 DOM Refs 暴露給父組件?
在極少數情況下,你可能希望在父組件中引用子節點的 DOM 節點。通常不建議這樣做,因為它會打破組件的封裝,但它偶爾可用於觸發焦點或測量子 DOM 節點的大小或位置。
如何將 ref 暴露給父組件?
如果你使用 16.3 或更高版本的 React, 這種情況下我們推薦使用 ref 轉發。Ref 轉發使組件可以像暴露自己的 ref 一樣暴露子組件的 ref。
什麼是 ref 轉發?
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 你可以直接獲取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
如果在低版本中如何轉發?
如果你使用 16.2 或更低版本的 React,或者你需要比 ref 轉發更高的靈活性,你可以使用 ref 作為特殊名字的 prop 直接傳遞。
比如下面這樣:
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.inputElement = React.createRef();
}
render() {
return (
<CustomTextInput inputRef={this.inputElement} />
);
}
}
以下是對上述示例發生情況的逐步解釋:
- 我們通過調用 React.createRef 創建了一個 React ref 並將其賦值給 ref 變數。
- 我們通過指定 ref 為 JSX 屬性,將其向下傳遞給
<FancyButton ref={ref}>
。 - React 傳遞 ref 給 forwardRef 內函數 (props, ref) => …,作為其第二個參數。
- 我們向下轉發該 ref 參數到
<button ref={ref}>
,將其指定為 JSX 屬性。 - 當 ref 掛載完成,ref.current 將指向
<button>
DOM 節點。
最後
歡迎關注公眾號「前端進階課」認真學前端,一起進階。回復 全棧
或 Vue
有好禮相送哦