Refs 在React中的應用

銅板街技術發表於2019-03-29

在介紹 Refs 之前,我們先來了解兩個概念:受控元件 和 不受控元件。

受控元件

在HTML中,表單元素(如 input、textarea、select)之類的表單元素通常可以自己維護state,並根據使用者的輸入進行更新。而在React中,可變狀態(mutable state)通常儲存在元件的 state 屬性中,並且只能通過 setState()來更新。 在此,我們將 React的state作為唯一的資料來源,通過渲染表單的React元件來控制使用者輸入過程中表單傳送的操作。 這個“被React通過此種方式控制取值的表單輸入元素”被成為受控元件

不受控制元件

從字面意思來理解:不被React元件控制的元件。在受控制元件中,表單資料由 React元件處理。其替代方案是不受控制元件,其中表單資料由DOM本身處理。檔案輸入標籤就是一個典型的不受控制元件,它的值只能由使用者設定,通過DOM自身提供的一些特性來獲取。

受控組件和不受控元件最大的區別就是前者自身維護的狀態值變化,可以配合自身的change事件,很容易進行修改或者校驗使用者的輸入。

在React中 因為 Refs的出現使得 不受控制元件自身狀態值的維護變得容易了許多,接下來我們就重點介紹一下 Refs的使用方式。

什麼是Refs

Refs 是一個 獲取 DOM節點或 React元素例項的工具。在 React 中 Refs 提供了一種方式,允許使用者訪問DOM 節點或者在render方法中建立的React元素。

在 React單項資料流中,props是父子元件互動的唯一方式。要修改一個子元件,需要通過的新的props來重新渲染。 但是在某些情況下,需要在資料流之外強制修改子元件。被修改的子元件可能是一個React元件例項,也可能是一個DOM元素。對於這兩種情況,React 都通過 Refs的使用提供了具體的解決方案。

使用場景

refs 通常適合在一下場景中使用:

  1. 對DOM 元素焦點的控制、內容選擇或者媒體播放;
  2. 通過對DOM元素控制,觸發動畫特效;
  3. 通第三方DOM庫的整合。

避免使用 refs 去做任何可以通過宣告式實現來完成的事情。例如,避免在Dialog、Loading、Alert等元件內部暴露 open(), show(), hide(),close()等方法,最好通過 isXX屬性的方式來控制。

使用方式

關於refs的使用有兩種方式: 1)通過 React.createRef() API【在React 16.3版本之後引入了】;2)在較早的版本中,我們推薦使用 回撥形式的refs。

React.createRef()

class TestComp extends React.Component {
  constructor(props) {
    super(props);
    this.tRef = React.createRef();
  }
  render() {
    return (
      <div ref={ this.tRef }></div>
    )
  }
}

複製程式碼

以上程式碼 建立了一個例項屬性 this.tRef, 並將其 傳遞給 DOM元素 div。後續對該節點的引用就可以在ref的 current屬性中訪問。ref的值根據節點型別的不同結果也不同:

  1. 當ref屬性用於普通 HTML 元素時,建構函式中使用 React.createRef() 建立的 ref 接收底層 DOM 元素作為其 current 屬性。
class TestComp extends React.Component {
  constructor(props) {
    super(props);
    // 建立一個 ref 來儲存 DOM元素 input
    this.textInput = React.createRef();
    this.focusEvent = this.focusEvent.bind(this);
  }
  focusEvent() {
    // 直接通過原生API訪問輸入框獲取焦點事件
    this.textInput.current.focus();
  }
  render() {
    return (
      <div>
        <input type="text" ref={this.textInput} />
        <input type="button" value="獲取文字框焦點事件" onClick={this.focusEvent}/>
      </div>
    );
  }
}

複製程式碼
  1. 當 ref 屬性用於自定義 class 元件時,ref 物件接收元件的掛載例項作為其 current 屬性。
class ParentComp extends React.Component {
  constructor(props) {
    super(props);
    // 建立ref 指向 ChildrenComp 元件例項
    this.textInput = React.createRef();
  }

  componentDidMount() {
    // 呼叫子元件 focusTextInput方法 觸發子元件內部 文字框獲取焦點事件
    this.textInput.current.focusTextInput();
  }

  render() {
    return (
      <ChildrenComp ref={ this.textInput } />
    );
  }
}
複製程式碼

class ChildrenComp extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  focusTextInput() {
    this.inputRef.current.focus();
  }
  render(){
    return(
      <div>
        <input type='text' value='父元件通過focusTextInput()方法控制獲取焦點事件' ref={ this.inputRef }/>
      </div>
    )
  }
}

複製程式碼
  1. 不能在函式元件上使用 ref 屬性,因為他們沒有例項。
回撥 Refs

React 也支援另外一種使用 refs的方式成為 “回撥 refs”,可以幫助我們更精準的控制何時 refs被設定和解除。 這個回撥函式中接受 React 元件例項或 HTML DOM 元素作為引數,以使它們能在其他地方被儲存和訪問。


class TestComp extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = null;
    // 使用'ref'的回撥函式將 text輸入框DOM節點的引用繫結到 React例項 this.textInput上
    this.inputRef = element => {
      this.textInput = element;
    }
    this.focus = () => {
      if (this.textInput) {
        this.textInput.focus();
      }
    }
  }
  componentDidMount() {
    this.focus();
  }
  render() {
    return (
      <div>
        <input type='text' ref={ this.inputRef } />
      </div>
    );
  }
}
複製程式碼

React 將在元件掛載時會呼叫 ref 回撥函式並傳入DOM 元素,當解除安裝時呼叫它並傳入 null。 在 componentDidMount 或 componentDidUpdate 觸發前,React 會保證 refs 一定是最新的。 在類元件中,通常父元件 把它的refs回撥函式 通過props的形式傳遞給子元件,同時子元件把相同的函式作為特殊的 ref屬性 傳遞給對應的 DOM 元素。

如果 ref 回撥函式是以行內函數的方式定義的,在更新過程中它會被執行兩次,第一次傳入引數 null,然後第二次會傳入引數 DOM 元素。這是因為在每次渲染時會建立一個新的函式例項,所以 React 清空舊的 ref 並且設定新的。通過將 ref 的回撥函式定義成 class 的繫結函式的方式可以避免上述問題,但是大多數情況下它是無關緊要的。

class TestComp extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = null;
    // 初始化 flag 值為 init
    this.state = {
      flag: 'init'
    }
    this.focus = () => {
      if (this.textInput) {
        this.textInput.focus();
      }
    }
  }
  componentDidMount() {
    this.focus();
    // 當執行完 render 首次渲染之後,更新狀態 flag 值 為 update
    this.setState({
      flag: 'update'
    });
  }
  render() {
    return (
      <div>
      {/* 通過內聯回撥形式定義 ref  */}
      <input type='text' value={this.state.flag} ref={(element) => {
        console.log('element', element); // 將傳入的 element 輸出控制檯
        this.textInput = element;
      }} />
      </div>
    )
  }
}

複製程式碼

Refs 在React中的應用

過時 API:String 型別的Refs

如果你目前還在使用 this.refs.textInput 這種方式訪問refs,官方建議使用 回撥函式 或者 createRef API的方式來替換。

如何將DOM 通過Refs 暴露給父元件

在極少數情況下,我們可能希望在父元件中引用子節點的 DOM 節點(官方不建議這樣操作,因為它會打破元件的封裝),使用者觸發焦點或者測量子DOM 節點的大小或者位置。雖然我們可以通過向子元件新增 ref的方式來解決,但這並不是一個理想的解決方案,因為我們只能獲取元件例項而不是 DOM節點。並且它還在函式元件上無效。

在react 16.3 或者更高版本中,我們推薦使用 ref 轉發的方式來實現以上操作。

ref 轉發使得元件可以像暴露自己的 ref一樣暴露子元件的 ref。

Ref forwarding is a technique for automatically passing a ref through a component to one of its children. This is typically not necessary for most components in the application. However, it can be useful for some kinds of components, especially in reusable component libraries.

Ref forwarding 是一種自動將ref 通過元件傳遞給其子節點的技術。下面我們通過具體的案例來演示一下效果。

const ref = React.createRef();
const BtnComp = React.forwardRef((props, ref) => {
  return (
    <div>
      <button ref={ref} className='btn'>
        { props.children }
      </button>
    </div>
  )
});

class TestComp extends React.Component {
  clickEvent() {
    if (ref && ref.current) {
      ref.current.addEventListener('click', () => {
        console.log('hello click!')
      });
    }
  }
  componentDidMount() {
    console.log('當前按鈕的class為:', ref.current.className); // btn
    this.clickEvent(); // hello click!
  }
  render() {
    return (
      <div>
        <BtnComp ref={ref}>點選我</BtnComp>
      </div>
    );
  }
}

複製程式碼

Refs 在React中的應用

上述案例,使用的元件BtnComp 可以獲取對底層 button DOM 節點的引用並在必要時對其進行操作,就像正常的HTML元素 button直接使用DOM一樣。

注意事項

第二個ref引數僅在使用React.forwardRef 回撥 定義元件時存在。常規函式或類元件不接收ref引數,並且在props中也不提供ref。

Ref轉發不僅限於DOM元件。您也可以將refs轉發給類元件例項。

高階元件中的refs

高階元件(HOC)是React中用於重用元件邏輯的高階技術,高階元件是一個獲取元件並返回新元件的函式。下面我們通過具體的案例來看一下refs如何在高階元件鐘正常使用。


// 記錄狀態值變更操作
function logProps(Comp) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }
    render() {
      const { forwardedRef, ...rest } = this.props;
      return <Comp ref={ forwardedRef } {...rest} />;
    }
  }
  return React.forwardRef((props, ref) => {
    return <LogProps { ...props } forwardedRef={ ref } />;
  });
}

// 子元件
const BtnComp = React.forwardRef((props, ref) => {
  return (
    <div>
      <button ref={ref} className='btn'>
        { props.children }
      </button>
    </div>
  )
});

// 被logProps包裝後返回的新子元件
const NewBtnComp = logProps(BtnComp);


class TestComp extends React.Component {
  constructor(props) {
    super(props);
    this.btnRef = React.createRef();

    this.state = {
      value: '初始化'
    }
  }

  componentDidMount() {
    console.log('ref', this.btnRef);
    console.log('ref', this.btnRef.current.className);
    this.btnRef.current.classList.add('cancel'); // 給BtnComp中的button新增一個class
    this.btnRef.current.focus(); // focus到button元素上
    setTimeout(() => {
      this.setState({
        value: '更新'
      });
    }, 10000);
  }

  render() {
    return (
      <NewBtnComp ref={this.btnRef}>{this.state.value}</NewBtnComp>
    );
  }
}

複製程式碼

最終的效果圖如下:

Refs 在React中的應用

Refs 在React中的應用


註明:文章來源於公眾號 react_native, 已經過作者授權轉載。


Refs 在React中的應用

更多精彩內容,請掃碼關注 “銅板街技術” 微信公眾號。

相關文章