前言
本文為意譯,翻譯過程中摻雜本人的理解,如有誤導,請放棄繼續閱讀。
原文地址:Refs and the DOM
正文
Refs提供了一種訪問在render方法裡面建立的React element或者原生DOM節點的方法。
在典型的React資料流中(自上而下的資料流),props是父元件與子元件打交道的唯一途徑。為了與子元件互動,你需要給子元件傳遞一個新的props,促使它重新渲染。然而,有不少的場景需要我們在這種props主導型的資料流之外去命令式地去修改子元件裡面的東西。被修改的子元件有可能是一個React component的例項,也有可能是一個原生DOM元素。對於這兩種情況,React都提供了一個“安全艙口”去訪問它們。
什麼時候用Refs呢?
以下幾個業務場景是挺合適的:
- 手動管理聚焦(focus),文字選擇或者視訊,音訊的回放。
- 命令式地觸發動畫。
- 與第三方的DOM類庫進行整合。
如果能用宣告式的方式去實現的,就不要用refs去實現。舉個例子說,能通過傳遞一個isOpen
prop給Dialog
元件來實現彈窗的開啟和關閉,就不用通過暴露“open”和“close”方法來實現彈窗的開啟和關閉(在結合redux資料流的背景下,我不太認同這句話所表達的觀點)。
不要濫用Refs
在實際開發的過程中,如果實現上遇到困難了,你的慣性思維可能是,先使用refs實現了它(這個功能),管它三七二十一呢。在這種情況下,你不妨先讓你的頭腦冷靜下來,以更縝密的思維去想想,能不能通過state來實現呢?如果能,state應該存放在元件樹層級中的哪個層級呢?一般來說,公認為合適存放state的層級是頂級元件,也就是我們以前說的“container component”。檢視提升你的state 看看該怎麼做。
注意,接下來的例子已經被更新過了。更新過後的例子使用了React.createRef()這個API。這個API在React 16.3中就引入了。假如你還在使用較早的React版本,那麼我們推薦你使用callback refs來代替。
Object Refs
建立Refs
使用React.createRef()來建立refs,並通過ref屬性來attached to React element。一般的做法是,在元件的constructor裡面,將通過React.createRef()來建立的refs直接賦值給元件的一個例項屬性。這樣一來,當元件例項化的時候,你就可以在元件的其他地方使用這個引用。(也就是說,元件例項的某個屬性是引用著通過React.createRef()來建立的Refs的,而這個refs又是通過ref屬性附加在原生DOM元素或者子元件例項上的。故通過這個例項屬性,我們是可以訪問到原生DOM元素或者子元件例項的)
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
複製程式碼
訪問Refs
上面講了如何建立refs,這一小節我們就來講如何訪問refs。其實上面已經講到了,我們通過元件的例項屬性來儲存著refs的引用的。refs是一個物件,它有一個current屬性。我們正是通過這個current屬性來訪問原生DOM元素或者子元件例項的。
const node = this.myRef.current;
複製程式碼
current的屬性值因不同的情況而異。這個不同的情況指的是ref屬性所在的React component的型別。
- DOM component。當ref屬性是負載在DOM component上的話,通過React.createRef()來建立的Refs物件的current屬性的值將會是原生的DOM元素。
- custom component。custom component又可以分為class component 和function component。注意,因為function component是沒有例項的,所以,它是不能通過這種方式來使用ref屬性的(這裡,這種方式是指上面“建立refs”這一小節所說的方法。實際上function component也是可以消費ref屬性的,這得使用後面提到的“ ref forwarding”技術)。所以這裡,custom component指的是class component。當ref屬性是掛載在custom component上的話,通過React.createRef()來建立的refs物件的current屬性將會指向custom component的元件例項。
下面的例子將會演示這兩者之間的不同。
1)把ref屬性掛載在DOM component上
下面的程式碼使用了ref屬性來儲存DOM元素的引用:
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// create a ref to store the textInput DOM element
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}
focusTextInput() {
// Explicitly focus the text input using the raw DOM API
// Note: we're accessing "current" to get the DOM node
this.textInput.current.focus();
}
render() {
// tell React that we want to associate the <input> ref
// with the `textInput` that we created in the constructor
return (
<div>
<input
type="text"
ref={this.textInput} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
複製程式碼
當ref屬性所在的那個元件掛載到頁面後,current屬性將會被賦值為指向原生DOM元素的引用。當這個元件被解除安裝後,current屬性又會被重置為null。ref屬性值的更新發生在元件生命週期函式componentDidMount
和componentDidUpdate
之前。
2)把ref屬性掛載在class component上
如果你想把上面的<CustomTextInput>
包裹在父元件中,並想模擬元件掛載後就自動獲取焦點。那麼,我們可以通過ref去訪問那個<CustomTextInput>
的例項,通過這個例項的focusTextInput
方法手動地讓對應的元件內部的input框獲得焦點。
class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
componentDidMount() {
// 在這裡this.textInput.current指向的是
// CustomTextInput元件的例項
this.textInput.current.focusTextInput();
}
render() {
return (
<CustomTextInput ref={this.textInput} />
);
}
}
複製程式碼
注意,<CustomTextInput>
元件是class component時,這種寫法才會有用。
class CustomTextInput extends React.Component {
// ...
}
複製程式碼
3)Refs與function component的關聯
注意,第三點,我們已經不使用“把ref屬性掛載在xxx元件上”這個說法了。因為把ref屬性直接掛載function component是沒有什麼用的。這裡用“關聯”一詞,只不過在表達,refs還是可以跟function component結合起來使用的。 再次強調,應為function component沒有例項,所以不要直接在function component上掛載ref屬性:
function MyFunctionComponent() {
return <input />;
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
render() {
// This will *not* work!
return (
<MyFunctionComponent ref={this.textInput} />
);
}
}
複製程式碼
如果你想在一個元件上直接掛載ref屬性,那麼你需要將這個元件轉化為class component。這種轉化,就像你如果需要使用生命週期函式或者state,你也會將元件轉化為class component一樣。
然而,正如我們上面提到的,ref屬性還是可以跟function component結合使用的。如何結合法呢?那就是在function component的實現程式碼體裡面使用。值得注意的是,即使在function component裡面去使用,ref屬還是要掛載在DOM component或者class component上:
function CustomTextInput(props) {
// textInput must be declared here so the ref can refer to it
let textInput = React.createRef();
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
複製程式碼
將DOM Refs暴露給父元件
在很少的情況下,你可能想直接從父元件來訪問子元件裡面的DOM元素。而一般情況下,我們是不推薦大家這麼做的。因為這麼做會打破元件封裝的完整性。但是呢,偶爾這麼做還是挺管用的。比如想手動讓輸入框獲取焦點或者測量子元件中DOM元素的位置和尺寸大小。
如果你正在使用React 16.3或者以上,我們推薦你使用ref forwarding來滿足你的需要。Ref forwarding技術讓子元件自己選擇是否要把ref引用暴露給父元件(Ref forwarding lets components opt into exposing any child component’s ref as their own)。你可以查閱一下ref forwarding文件。這裡的例子將會給你演示如何地將子元件中DOM元素的ref引用暴露給父元件的。
如果你再用React 16.2或者以下,或者需要一個比ref fowarding更加靈活的方案,你可以使用這個方案。通過一個不同與ref的prop名,顯式地將ref引用傳遞下去。
我們建議儘可能少地去暴露DOM元素給外界。但是,它確實可以是一個很有用的(訪問原生DOM)“安全艙口”。注意,這種訪問原生DOM的方案需要你往子元件中新增一些程式碼。假如你對子元件的實現沒有控制權(即往裡面插入一些程式碼),那麼這個時候你只剩下最後的選擇了-使用findDOMNode()。原則上,findDOMNode()已經不被鼓勵使用了。在 StrictMode下,這個API已經被廢棄了。
Callback Refs
除了上面提到的方法外,React也支援別的方式去設定refs,其中一個就叫“callback refs”。“callback refs”能夠在refs賦新值和重置的時候給你更小粒度的控制權。
相比於使用createRef()來建立refs並將它傳遞給ref屬性,“callback refs”傳遞給ref屬性的是一個函式,準備來說是一個callback函式。在這個callback函式裡面,你可以通過引數獲得一個訪問React component例項或者原生的DOM元素的引用。一般的做法,使用一個元件的例項屬性來儲存這個引用,方便到處使用:
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = null;
this.setTextInputRef = element => {
this.textInput = element;
};
this.focusTextInput = () => {
// Focus the text input using the raw DOM API
if (this.textInput) this.textInput.focus();
};
}
componentDidMount() {
// autofocus the input on mount
this.focusTextInput();
}
render() {
// Use the `ref` callback to store a reference to the text input DOM
// element in an instance field (for example, this.textInput).
return (
<div>
<input
type="text"
ref={this.setTextInputRef}
/>
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
複製程式碼
當元件掛載到頁面後,React將會給這個callback函式傳入個一個元件例項或者原生DOM的引用;當元件解除安裝後,React再次給callback函式傳入null(這裡說的“給callback函式傳入”代表著一次callback的呼叫)。React保證refs的更新會在componentDidMount和componentDidUpdate呼叫之前發生。
正如由React.createRef()建立出來的物件型別refs一樣,你可以將函式型別的refs一路傳遞下去。
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}
class Parent extends React.Component {
render() {
return (
<CustomTextInput
inputRef={el => this.inputElement = el}
/>
);
}
}
複製程式碼
在上面的例子中,父元件<Parent>
將函式型別的refs以一個叫inputRef的prop傳遞給<CustomTextInput>
元件。然後<CustomTextInput>
元件用真正的ref屬性傳遞給<input>
元件。從結果來看,<Parent>
的例項屬性inputElement引用的正是我們想要訪問的,<CustomTextInput>
元件裡面的<input>
DOM元素。
Legacy API: String Refs
如果你之前有使用過React,那麼你應該知道ref屬性的值可以是一個字串,比如:“textInput”。然後你可以通過this.refs.textInput來訪問原生的DOM元素。我們強烈建議你不要再使用它了。因為它存在某些問題,同時它已經被遺棄了。在未來的某個版本中,很有可能會把它的實現從程式碼中移除掉。
注意,如果你現在正在使用this.refs.textInput來訪問refs,那麼我們推薦你使用callback pattern或者createRef API來代替。
使用callback refs的注意點
如果你把一個inline function直接賦值給ref屬性的話,那麼這個inline function將會被呼叫兩次。第一次是以null來呼叫的。第二次才是以真實的DOM元素去呼叫。這是因為,每一次render方法被呼叫的時候,inline function都會建立一個新的函式例項給ref屬性。所以,React需要先移除舊的ref callback,再來設定新的。為了避免這個問題,你可以通過把這個inline function繫結成class component的方法,使之成為引用,然後將這個引用賦值給ref屬性。不過大多數情況下,inline function不會造成什麼大問題的。