[譯]React高階話題之Refs and the DOM

鯊叔發表於2018-12-05

前言

本文為意譯,翻譯過程中摻雜本人的理解,如有誤導,請放棄繼續閱讀。

原文地址:Refs and the DOM

正文

Refs提供了一種訪問在render方法裡面建立的React element或者原生DOM節點的方法。

在典型的React資料流中(自上而下的資料流),props是父元件與子元件打交道的唯一途徑。為了與子元件互動,你需要給子元件傳遞一個新的props,促使它重新渲染。然而,有不少的場景需要我們在這種props主導型的資料流之外去命令式地去修改子元件裡面的東西。被修改的子元件有可能是一個React component的例項,也有可能是一個原生DOM元素。對於這兩種情況,React都提供了一個“安全艙口”去訪問它們。

什麼時候用Refs呢?

以下幾個業務場景是挺合適的:

  • 手動管理聚焦(focus),文字選擇或者視訊,音訊的回放。
  • 命令式地觸發動畫。
  • 與第三方的DOM類庫進行整合。

如果能用宣告式的方式去實現的,就不要用refs去實現。舉個例子說,能通過傳遞一個isOpenprop給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屬性值的更新發生在元件生命週期函式componentDidMountcomponentDidUpdate之前。

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不會造成什麼大問題的。

相關文章