React 重溫之 Refs

什麼是Refs

咱們在平常寫React代碼的時候,通常狀況是用不到Refs這個東西,由於咱們並不直接操做底層DOM元素,而是在render函數裏去編寫咱們的頁面結構,由React來組織DOM元素的更新。node

凡事總有例外,總會有一些很奇葩的時候咱們須要直接去操做頁面的真實DOM,這就要求咱們有直接訪問真實DOM的能力,而Refs就是爲咱們提供了這樣的能力。數組

看這個名字也知道,Refs實際上是提供了一個對真實DOM(組件)的引用,咱們能夠經過這個引用直接去操做DOM(組件)app

爲何會用到這個

上面有提到,咱們通常狀況下是不須要用到這個東西,那具體何時纔會用到呢? 看官方建議:ide

- Managing focus, text selection, or media playback.
 - Triggering imperative animations.
 - Integrating with third-party DOM libraries.

簡單的來講就是處理DOM元素的focus,文本的選擇或者媒體的播放等,以及處罰強制動畫或者同第三方DOM庫集成的時候。函數

也就是React沒法控制局面的時候,就須要直接操做Refs了。動畫

怎麼用

React V16版本以前,

咱們通常都是經過一個回調函數的方式,把當前組件的DOM綁定到一個實例變量上,像下面這樣:this

class AutoFocusTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = null;
  }

  componentDidMount() {
    this.textInput.focusTextInput();
  }

  render() {
    return (
      <CustomTextInput ref={ele => { this.textInput = ele}} />
    );
  }
}

在上面的代碼中,咱們先聲明一個值爲null的textInput變量,而後在ref中以回調的方式將組件DOM賦值給textInput。而後就能夠經過 this.textInput.focus()這樣的性質來直接調用CustomTextInput這個組件的實例方法。rest

可是這個方式有如下兩個不太好:code

  1. 每次組件從新渲染的時候,行內函數都會執行兩次,第一次的ele的值爲空,第二次才爲真正的DOM對象。

    由於在每次渲染中React都會建立一個新的函數實例。所以,React 須要清理舊的 ref 而且設置新的。
    經過將 ref 的回調函數定義成類的綁定函數的方式能夠避免上述問題,component

  2. 若是咱們想要將一個子組件的ref傳遞給父組件,可能會有點麻煩,雖然經過一個特殊的prop屬性能夠作到,可是感受有點不太正規。。。

React V16 版本後

React V16版本新增一個API:React.createRef(); 經過這個API,咱們能夠先建立一個ref變量,而後再將這個變量賦值給組件聲明中ref屬性就行了。

具體看代碼:

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>
    );
  }
}

在上面的代碼中,咱們先經過 React.createRef();建立一個ref,並賦值給組件屬性textInput(this.textInput),而後在render函數中經過ref={this.textInput}的方式將ref和input這個真實DOM聯繫起來, 這樣就能夠經過 this.textInput.current.focus();的方式來直接操做input元素的方法。

不一樣之處

在V16版本前,咱們能夠直接經過變量訪問元素的方法,在V16後,咱們須要經過 this.textInput.current,即真實的DOM是經過current屬性來引用的。

若是經過 createRef()這個API賦值給組件的ref,那麼引用的就是組件實例;若是是DOM元素,那引用的天然的就是DOM元素了。。

傳遞Refs

前面咱們說到,在V16版本以前,咱們想要父組件拿到子組件的ref,須要經過一些特殊的方法,V16版本以後,React提供了一種原生的方式來完成這種操做。

這就涉及到React新增的另外一個API: React.forwardRef(), 經過接受一個函數,來傳遞refs,具體以下:

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
  1. 首先咱們經過React.createRef();建立一個ref變量,而後在FancyButton屬性中經過 ref={ref}的方式把這個ref和組件關聯起來。
  2. 目前爲止,若是FancyButton 是一個經過class或者函數聲明的組件,那麼就到此爲止,咱們能夠說 ref變量的current屬性持有對 FancyButton組件實例的引用。
  3. 不幸的是,FancyButton通過了 React.forwardRef的處理, 這個API接受兩個參數,第二個參數就是ref,而後經過 <button ref={ref}>把ref綁定到button元素上,這樣ref.current的引用就是button元素這個DOM對象了。。。

上面的有點繞,簡單來講,就是咱們建立一個引用,原本是給外面的FancyButton組件的,可是由於React.forwardRef的處理,這個引用被傳遞給了內部的button元素。這樣ref.current的引用由原本的FancyButton實例傳遞到了button元素自己。

在HOC組件中的應用

HOC(higher-order components)高階組件,簡單的說,就是經過組件包裹的方式來提到代碼複用,高階組件就是一個函數,且該函數接受一個組件做爲參數,並返回一個新的組件。

如下是一個生成高階組件的函數:

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
}

logProps是函數,接受一個組件參數,返回一個包裹參數組件的logProps組件。

下面是用法:

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// Rather than exporting FancyButton, we export LogProps.
// It will render a FancyButton though.
export default logProps(FancyButton);

咱們先聲明一個FancyButton的組件,而後將其做爲參數傳入logProps函數,最後獲得的實際上是一個LogProps組件。

接下來咱們使用refs:

import FancyButton from './FancyButton';

const ref = React.createRef();

// The FancyButton component we imported is the LogProps HOC.
// Even though the rendered output will be the same,
// Our ref will point to LogProps instead of the inner FancyButton component!
// This means we can't call e.g. ref.current.focus()
<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}
/>;

咱們經過文件引入FancyButton(其實引入的是LogProps組件)而後createRef並指向FancyButton。 本意是但願引入真正的FancyButton組件,實際上引用的是 外層包裹組件LogProps組件。

咱們能夠經過如下改造來完善代碼:

function logProps(Component) {
  class LogProps extends React.Component {
    render() {
      const {forwardedRef, ...rest} = this.props;

      // Assign the custom prop "forwardedRef" as a ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // Note the second param "ref" provided by React.forwardRef.
  // We can pass it along to LogProps as a regular prop, e.g. "forwardedRef"
  // And it can then be attached to the Component.
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

如面的代碼所示,咱們修改了高階組件logProps函數的實現方式,在內部組件LogProps的render方法中,給被包裹組件(做爲參數傳入的組件)添加了來自props的ref。

最終返回的也是一個React.forwardRef處理過的組件,這個組件將ref傳遞到內部的props中去。

這樣,但咱們經過logProps(FancyButton)函數調用的時候,其實返回的是一個通過React.forwardRef處理的組件, 當經過

<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}
/>;

去添加ref的時候, 這個ref其實直接添加到了內部的LogProps組件的forwardedRef屬性上,而後在LogProps組件內部,又經過props屬性的方式被賦值了 被包裹組件(做爲參數的組件,也就是FancyButton組件)。這個傳遞其實通過了三次。。。。

總的來講,高階組件的ref實際上是經過React.forwardRef技術將ref傳遞到包裹組件logProps上,而後有經過屬性傳遞 傳遞到真正的FancyButton組件上,兩次傳遞才完成。。。。

相關文章
相關標籤/搜索