React組件/元素與實例分析

做者:Dan Abramov
譯者:Jogis
譯文連接:https://github.com/yesvods/Blog/issues/5
轉載請註明譯文連接以及譯者信息html

前言

不少React新手對Components以及他們的instances和elements之間的區別感到很是困惑,爲何要用三種不一樣的術語來表明那些被渲染在熒屏上的內容呢?react

親自管理實例(Managing the Instances)

若是是剛入門React,那麼你應該只是接觸過一些組件類(component classes)以及實例(instances)。打比方,你可能經過class關鍵字聲明瞭一個Button組件。這個程序運行時候,可能會有幾個Button組件的實例(instances)運行在瀏覽器上,每個實例會有各自的參數(properties)以及本地狀態(state)。這種屬於傳統的面向對象UI編程。那麼爲何會有元素(elements)出現呢?git

在這種傳統UI模式上,你須要負責建立和刪除實例(instances)的子組件實例。若是一個Form的組件想要渲染一個Button子組件,須要實例化這個Button子組件,而且手動更新他們的內容。github

class Form extends TraditionalObjectOrientedView {
  render() {
    // Read some data passed to the view
    const { isSubmitted, buttonText } = this.attrs;

    if (!isSubmitted && !this.button) {
      // Form is not yet submitted. Create the button!
      this.button = new Button({
        children: buttonText,
        color: 'blue'
      });
      this.el.appendChild(this.button.el);
    }

    if (this.button) {
      // The button is visible. Update its text!
      this.button.attrs.children = buttonText;
      this.button.render();
    }

    if (isSubmitted && this.button) {
      // Form was submitted. Destroy the button!
      this.el.removeChild(this.button.el);
      this.button.destroy();
    }

    if (isSubmitted && !this.message) {
      // Form was submitted. Show the success message!
      this.message = new Message({ text: 'Success!' });
      this.el.appendChild(this.message.el);
    }
  }
}

這個只是僞代碼,可是這個就是大概的形式。特別是當你用一些庫(好比Backbone),去寫一些須要保持數據同步的組件化組合的UI界面時候。編程

每個組件實例須要保留它的DOM節點引用和子組件的實例,而且須要在合適時機去建立、更新、刪除那些子組件實例。代碼行數會隨着組件的狀態(state)數量,以平方几何級別增加。並且這樣,組件須要直接訪問它的子組件實例,使得這個組件之後很是難解耦。react-native

因而,React又有什麼不一樣呢?api

用元素來描述節點樹(Elements Describe the Tree)

React提出一種元素(elements)來解決這個問題。一個元素僅僅是一個純的JSON對象,用於描述這個組件的實例或者是DOM節點(譯者注:好比div)和組件所須要的參數。元素僅僅包括三個信息:組件類型(例如,Button)、組件參數(例如:color)和一些組件的子元素數組

一個元素(element)實際上並不等於組件的實例,更確切地說,它是一種方式,去告訴React在熒屏上渲染什麼,你並不能調用元素的任何方法,它僅僅是一個不可修改的對象,這個對象帶有兩個字段:type: (string | ReactClass)props: Object1瀏覽器

DOM元素(DOM Element)

當一個元素的type是一個字符串,表明是一個type(譯者注:好比div)類型的DOM,props對應的是這個DOM的屬性。React就是根據這個規則來渲染,好比:安全

{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      children: 'OK!'
    }
  }
}

這個元素只是用一個純的JSON對象,去表明下面的HTML:

<button class='button button-blue'>
  <b>
    OK!
  </b>
</button>

須要注意的是,元素之間是怎麼嵌套的。按照慣例,當咱們想去建立一棵元素樹(譯者注:對,有點拗口),咱們會定義一個或者多個子元素做爲一個大的元素(容器元素)的children參數。

最重要的是,父子元素都只是一種描述符,並非實際的實例(instances)。在他們被建立的時候,他們不會去引用任何被渲染在熒屏上的內容。你能夠建立他們,而後把他們刪掉,這並不會對熒屏渲染產生任何影響。

React元素是很是容易遍歷的,不須要去解析,理所固然的是,他們比真實的DOM元素輕量不少————由於他們只是純JSON對象。

組件元素(Component Elements)

然而,元素的type屬性可能會是一個函數或者是一個類,表明這是一個React組件:

{
  type: Button,
  props: {
    color: 'blue',
    children: 'OK!'
  }
}

這就是React的核心靈感!

一個描述另一個組件的元素,依舊是一個元素,就像剛剛描述DON節點的元素那樣。他們能夠被嵌套(nexted)和相互混合(mixed)。

這種特性可讓你定義一個DangerButton組件,做爲一個有特定Color屬性值的Button組件,而不須要擔憂Button組件實際渲染成DOM的適合是button仍是div,或者是其餘:

const DangerButton = ({ children }) => ({
  type: Button,
  props: {
    color: 'red',
    children: children
  }
});

在一個元素樹裏面,你能夠混合配對DOM和組件元素:

const DeleteAccount = () => ({
  type: 'div',
  props: {
    children: [{
      type: 'p',
      props: {
        children: 'Are you sure?'
      }
    }, {
      type: DangerButton,
      props: {
        children: 'Yep'
      }
    }, {
      type: Button,
      props: {
        color: 'blue',
        children: 'Cancel'
      }
   }]
});

或者可能你更喜歡JSX:

const DeleteAccount = () => (
  <div>
    <p>Are you sure?</p>
    <DangerButton>Yep</DangerButton>
    <Button color='blue'>Cancel</Button>
  </div>
);

這種混合配對有利於保持組件的相互解耦關係,由於他們能夠經過組合(componsition)獨立地表達is-a()has-a()的關係:

  • Button是一個附帶特定參數的<button>DOM

  • DangerButton是一個附帶特定參數的Button

  • DeleteAccount在一個<div>DOM裏包含一個Button和一個DangerButton

組件封裝元素樹(Components Encapsulate Element Trees)

當React看到一個帶有type屬性的元素,並且這個type是個函數或者類,React就會去把相應的props給予元素,而且去獲取元素返回的子元素。

當React看到這種元素:

{
  type: Button,
  props: {
    color: 'blue',
    children: 'OK!'
  }
}

就會去獲取Button要渲染的子元素,Button就會返回下面的元素:

{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      children: 'OK!'
    }
  }
}

React會不斷重複這個過程,直到它獲取這個頁面全部組件潛在的DOM標籤元素。

React就像一個孩子,會去問「什麼是 Y」,而後你會回答「X 是 Y」。孩子重複這個過程直到他們弄清楚這個世界的每個小的細節。

還記得上面提到的Form例子嗎?它能夠用React來寫成下面形式:

const Form = ({ isSubmitted, buttonText }) => {
  if (isSubmitted) {
    // Form submitted! Return a message element.
    return {
      type: Message,
      props: {
        text: 'Success!'
      }
    };
  }

  // Form is still visible! Return a button element.
  return {
    type: Button,
    props: {
      children: buttonText,
      color: 'blue'
    }
  };
};

就是這麼簡單!對於一個React組件,props會被做爲輸入內容,一個元素會被做爲輸出內容。

被組件返回的元素樹可能包含描述DOM節點的子元素,和描述其餘組件的子元素。這可讓你組合UI的獨立部分,而不須要依賴他們內部的DOM結構。

React會替咱們建立更新和刪除實例,咱們只須要經過組件返回的元素來描述這些示例,React會替咱們管理好這些實例的操做。

組件多是類或者函數(Components Can Be Classes or Functions)

在上面提到的例子裏,Form,MessageButton都是React組件。他們均可以被寫成函數形式,就像上面提到的,或者是寫成繼承React.Component的類的形式。這三種聲明組件的方法結果幾乎都是相同的:

// 1) As a function of props
// 1) 做爲一個接收props參數的函數
const Button = ({ children, color }) => ({
  type: 'button',
  props: {
    className: 'button button-' + color,
    children: {
      type: 'b',
      props: {
        children: children
      }
    }
  }
});

// 2) Using the React.createClass() factory
// 2) 使用React.createClass()的工廠方法
const Button = React.createClass({
  render() {
    const { children, color } = this.props;
    return {
      type: 'button',
      props: {
        className: 'button button-' + color,
        children: {
          type: 'b',
          props: {
            children: children
          }
        }
      }
    };
  }
});

// 3) As an ES6 class descending from React.Component
// 3) 做爲一個ES6的類,去繼承React.Component
class Button extends React.Component {
  render() {
    const { children, color } = this.props;
    return {
      type: 'button',
      props: {
        className: 'button button-' + color,
        children: {
          type: 'b',
          props: {
            children: children
          }
        }
      }
    };
  }
}

當組件被定義爲類,它會比起函數方法的定義強大一些。它能夠存儲一些本地狀態(state)以及在相應DOM節點建立或者刪除時候去執行一些自定義邏輯。

一個函數組件會沒那麼強大,可是會更簡潔,並且能夠經過一個render()就能表現得就像一個類組件同樣。除非你須要一些只能用類才能提供的特性,不然咱們鼓勵你去使用函數組件來替代類組件。

然而,不論是函數組件或者類組件,基原本說,他們都屬於React組件。他們都會以props做爲輸入內容,以元素做爲輸出內容

自頂向下的協調(Top-Down Reconciliation)

當你調用:

ReactDOM.render({
  type: Form,
  props: {
    isSubmitted: false,
    buttonText: 'OK!'
  }
}, document.getElementById('root'));

React會提供那些props去問Form:「請你返回你的元素樹」,而後他最終會使用簡單的方式,去「精煉」出他對於你的組件樹的理解:

// React: You told me this...
{
  type: Form,
  props: {
    isSubmitted: false,
    buttonText: 'OK!'
  }
}

// React: ...And Form told me this...
{
  type: Button,
  props: {
    children: 'OK!',
    color: 'blue'
  }
}

// React: ...and Button told me this! I guess I'm done.
{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}

這部分過程被React稱做協調(reconciliation),在你調用ReactDOM.render()或者setState()的時候會被執行。在協調過程結束以前,React掌握DOM樹的結果,再這以後,好比react-dom或者react-native的渲染器會應用最小必要變動集合來更新DOM節點(或者是React Native的特定平臺視圖)。

這個逐步精煉的過程也說明了爲何React應用如此容易優化。若是你的組件樹一部分變得太龐大以致於React難以去高效訪問,在相關參數沒有變化的狀況下,你能夠告訴React去跳過這一步「精煉」以及跳過diff樹的其中一部分。若是參數是不可修改的,計算出他們是否有變化會變得至關快。因此React和immutability結合起來會很是好,並且能夠用最小的代價去得到最大的優化。

你可能發現這篇博客一開始談論到不少關於組件和元素的內容,可是並無太多關於實例的。事實上,比起大多數的面向對象UI框架,實例在React上顯得並無那麼重要。

只有類組件能夠擁有實例,並且你歷來不須要直接建立他們:React會幫你作好。當存在父組件實例訪問子組件實例的狀況下,他們只是被用來作一些必要的動做(好比在一個表單域設置焦點),並且一般應該要避免這樣作。

React爲每個類組件維護實例的建立,因此你能夠以面向對象的方式,用方法和本地狀態去編寫組件,可是除此以外,實例在React的變成模型上並非很重要,並且會被React本身管理好。

總結

元素是一個純的JSON對象,用於描述你想經過DOM節點或者其餘組件在熒屏上展現的內容。元素能夠在他們的參數裏面包含其餘元素。建立一個React元素代價很是小。一個元素一旦被建立,將不可更改。

一個組件能夠用幾種不一樣的方式去聲明。能夠是一個帶有render()方法的類。做爲另一種選擇,在簡單的狀況下,組件能夠被定義爲一個函數。在兩種方式下,組件都是被傳入的參數做爲輸入內容,以返回的元素做爲輸出內容。

若是有一個組件被調用,傳入了一些參數做爲輸入,那是由於有一某個父組件返回了一個帶有這個組件的type以及這些參數(到React上)。這就是爲何你們都認爲參數流動方式只有一種:從父組件到子組件。

實例就是你在組件上調用this時候獲得的東西,它對本地狀態存儲以及對響應生命週期事件很是有用。

函數組件根本沒有實例,類組件擁有實例,可是你歷來都不須要去直接建立一個組件實例——React會幫你管理好它。

最後,想要建立元素,使用React.createElement(),JSX或者一個元素工廠工具。不要在實際代碼上把元素寫成純JSON對象——僅須要知道他們在React機制下面以純JSON對象存在就好。

更多相關內容


  1. 出於安全考慮,全部React元素須要在對象下聲明一個額外的 $$typeof:Symbol.for(‘react.element‘) 字段。它在上面的例子被忽略了。這篇博客從頭開始用行內對象來表示元素,來告知你底層運做的概念。可是,除非你要麼添加$$typeof 到元素上或者用 React.createElement 或JSX去修改上面的代碼,不然那些代碼並不能正常執行。
相關文章
相關標籤/搜索