React Ref or Not?

歡迎關注個人公衆號睿Talk,獲取我最新的文章:
clipboard.pngjavascript

1、前言

React的Ref特性是React聲明式編程(Declarative Programming)設計哲學的一個重要補充。以前對它的認識只是停留在非受控組件這種特殊場景,直到最近爲了實現項目中的一個特殊功能,纔對它有了更深的理解。java

2、什麼是Ref

React的官方解釋是這樣的:編程

In the typical React dataflow, props are the only way that parent components interact with their children. To modify a child, you re-render it with new props. However, there are a few cases where you need to imperatively modify a child outside of the typical dataflow. The child to be modified could be an instance of a React component, or it could be a DOM element. For both of these cases, React provides an escape hatch.

當中提到了幾個關鍵的概念。segmentfault

  • 在典型的React數據流理念中,父組件跟子組件的交互都是經過傳遞屬性(properties)實現的。若是父組件須要修改子組件,只須要將新的屬性傳遞給子組件,由子組件來實現具體的繪製邏輯。
  • 特殊的狀況下,若是你須要命令式(imperatively)的修改子組件,React也提供了應急的處理辦法--Ref
  • Ref既支持修改DOM元素,也支持修改自定義的組件。

3、什麼是聲明式編程(Declarative Programming)

值得一提的是當中聲明式編程(Declarative Programming)和命令式編程(Imperative Programming)的區別。聲明式編程的特色是隻描述要實現的結果,而不關心如何一步一步實現的,而命令式編程則相反,必須每一個步驟都寫清楚。以數組爲例,若是要打印出數組全部元素,聲明式編程是這麼寫:數組

let arr = [1,2,3];
const printElement = (element) => console.log(element);

arr.forEach( printElement );

而用命令式編程,會這麼寫:ide

let arr = [1,2,3];

for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}

經過對比,咱們能夠很直觀的感覺到聲明式編程的好處。代碼的核心功能就是這句:函數式編程

arr.forEach( printElement );

咱們能夠根據語義直觀的理解代碼的功能是:針對數組的每個元素,將它的值打印出來。沒必要關心實現其的細節。而命令式編程必須將每行代碼讀懂,而後再整合起來理解整體實現的功能。函數

React有2個基石設計理念:一個是聲明式編程,一個是函數式編程。函數式編程之後有機會再展開講。聲明式編程的特色體如今2方面:動畫

  • 組件定義的時候,全部的實現邏輯都封裝在組件的內部,經過state管理,對外只暴露屬性。
  • 組件使用的時候,組件調用者經過傳入不一樣屬性的值來達到展示不一樣內容的效果。一切效果都是事先定義好的,至於效果是怎麼實現的,組件調用者不須要關心。

所以,在使用React的時候,通常不多須要用到Ref。那麼,Ref的使用場景又是什麼?this

4、Ref使用場景

React官方文檔是這麼說的:

There are a few good use cases for refs:

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

Avoid using refs for anything that can be done declaratively.

意思是:

  • 控制一些DOM原生的效果,如輸入框的聚焦效果和選中效果等;
  • 觸發一些命令式的動畫;
  • 集成第三方的DOM庫。

最後還補了一句:若是要實現的功能能夠經過聲明式的方式實現,就不要藉助Ref。若是你就是那麼任性,要使用Ref,具體該怎麼作?

5、Ref用法

  • 若是做用在原生的DOM元素上,經過Ref獲取的是DOM元素,能夠直接操做DOM的API:
class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.focusTextInput = this.focusTextInput.bind(this);
  }

  focusTextInput() {
    // 獲取DOM元素後能夠直接操做DOM API
    this.textInput.focus();
  }

  render() {
    // 經過Ref獲取DOM元素,再保存在實例變量focusTextInput中
    return (
      <div>
        <input
          type="text"
          ref={(input) => { this.textInput = input; }} />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}
  • 若是做用在自定義組件,Ref獲取的是組件的實例,能夠直接操做組件內的任意方法:
// CustomTextInput組件的定義跟上面徹底相同
class AutoFocusTextInput extends React.Component {
  componentDidMount() {
    // 這裏直接調用CustomTextInput實例的focusTextInput方法
    this.textInput.focusTextInput();
  }

  render() {
    return (
      // 經過Ref獲取CustomTextInput實例,再保存在實例變量textInput中
      <CustomTextInput
        ref={(input) => { this.textInput = input; }} />
    );
  }
}

理解了基本使用後,再回到我遇到的真實場景。

6、Ref應用

先簡單描述下項目要實現的效果:在一個頁面中分左右兩部分,左邊顯示商品的列表,右邊顯示選中商品的購物車。一次能夠將左邊的多個商品,添加到右邊的購物車中。因爲具體的實現細節比較複雜,當時的分工是一我的實現左側的商品列表,另外一人負責右邊的購物車。若是用傳統的React設計理念來實現,必需要藉助左邊列表組件和右邊購物車組件的共同父組件,也就是頁面的根組件,來維護選中的商品數組。而後再將商品數組傳入購物車展現。這樣作的話實現起來很是不方便,要把購物車中的不少邏輯都放在父組件中,而實際上這些邏輯是購物車本身獨立使用的,跟其它組件並無交互。左側的列表組件只須要將選中的商品告知購物車便可,後續的邏輯由購物車本身實現。

考慮再三後,咱們決定經過Ref的方式將其內部的addProduct的方法暴露出來給父組件,當選中一個商品後,列表組件將商品信息傳遞給父組件,父組件再經過addProduct方法將商品信息傳入購物車。由購物車組件本身來維護客戶購買的全部商品數據。總體邏輯就是這樣,具體代碼此處略過,主要描述的是思路。也許你會問爲啥不將商品信息經過props傳入購物車組件?實現上是沒問題的,都能達到效果。但咱們認爲顯式的調用addProduct方法會更加直觀的表達語義,同時對addProduct方法也作了限制,只負責添加商品信息,不作更多的邏輯判斷。

若是說還有沒更好的實現方式,實際上是有的,能夠經過Redux來管理整個頁面的狀態。但引入Redux後,代碼的維護成本會隨之上升,目前暫時不做考慮。

7、總結

本文以項目中遇到的設計問題爲起點,介紹了React Ref特性的使用場景和具體的使用方法,順便還對比了聲明式編程和命令式編程2種編程風格,對React的設計理念做了簡要的解讀。

8、後記

這篇文章是八個月前寫的,隨着業務的快速發展,當初寫的代碼沒法承載新業務,要全盤重構了。重構的設計中,咱們引入了Redux來作狀態管理,組件之間的耦合度一會兒就降低了不少,複雜業務的實現也變得容易了。因此我認爲最優的實現方案是使用Redux,而不是Ref。

相關文章
相關標籤/搜索