剖析React內部運行機制-「譯」React組件、元素和實例

原文: React Components, Elements, and Instanceshtml


組件與它們實例以及元素之間的區別困擾着不少的React初學者。爲何會有三種不一樣的術語來指代屏幕上的東西?react


管理實例

若是你是一個React新手,你之前可能只使用過組件類和實例。好比,你經過建立一個class來聲明一個Button。app執行過程當中,在屏幕中你會獲得這個組件的幾個實例,每個都有本身的屬性和本地狀態。這是傳統的面向對象編程。那麼爲何要介紹元素呢?git

在傳統的UI模式中,你須要本身去建立和銷燬子組件實例。若是一個Form組件想要渲染一個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);
    }
  }
}
複製代碼

上面是Form組件的僞代碼,可是這些或多或少是你編寫組合UI代碼的最終結果,這些代碼使用一個庫好比BackBone,以面向對象的方式達到行爲一致性。算法

每一個組件實例必須保持對其DOM節點和子組件實例的引用,並在適當的時候建立、更新和銷燬它們。代碼行數將會朝着組件可能狀態數的平方量級增加,而且父組件能夠直接訪問其子組件實例,這使得未來很難解耦它們。編程

那麼,React有什麼不一樣呢?react-native

用元素來描述樹

在React中,這就是元素髮揮做用的地方。元素是描述組件實例或DOM節點及其所需屬性的普通對象。 它只包含關於組件類型(例如,按鈕)、屬性(例如,顏色)和其中一些子元素的信息。api

元素不是真實的實例,而是一種告訴React你想在屏幕上看到什麼的方式。咱們不能調用元素上面的任何方法,由於它只是一個不可變的描述對象,有兩個字段:type: (string | ReactClass)props: object數組

{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}
複製代碼

這個元素只是將下列HTML表示爲普通對象的一種方式:安全

<button class='button button-blue'>
  <b>
    OK!
  </b>
</button>
複製代碼

請注意元素是如何被嵌套的。按照慣例,在建立元素樹時,咱們指定一個或多個子元素做爲其包含元素的children屬性。

重要的是,子元素和父元素都只是描述,而不是真實的實例。當你建立它們時,它們不會指向屏幕上的任何東西。你能夠創造它們,而後把它們扔掉,這並不會產生其餘影響。

React元素很容易遍歷,不須要解析,而且它們比實際的DOM元素輕量得多——由於它們只是對象!

組件元素

事實上,元素的類型(type)也能夠是對應於React組件的函數(function)或類(class),就像下面這樣:

{
  type: Button,
  props: {
    color: 'blue',
    children: 'OK!'
  }
}
複製代碼

這是React的核心理念。

描述組件的元素也是元素,就像描述DOM節點的元素同樣。它們能夠相互嵌套和混合。

這個特色讓你定義一個DangerButton組件爲一個Button,能夠具備特定的color屬性,而不須要擔憂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>
);
複製代碼

這種混合和匹配有助於保持組件之間的解耦,由於它們能夠僅經過組合來表達「繼承」和「從屬」關係:

  • Button是一個具備特定屬性的DOM < Button >
  • DangerButton是一個具備特定屬性的Button
  • DeleteAccountdiv中包含了一個Button和一個DangerButton

組件封裝元素樹

當React看到一個帶有函數(function)或類(class)類型的元素時,它知道詢問該組件應該渲染哪一個元素,並給出相應的屬性。

當React看到下面這個元素時:

{
  type: Button,
  props: {
    color: 'blue',
    children: 'OK!'
  }
}
複製代碼

React會詢問Button應該渲染什麼。而後Button會返回下面的元素:

{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}
複製代碼

React將重複此過程,直到它知道頁面上每一個組件的底層DOM標記元素。

React就像一個孩子同樣,當你向他們解釋每個「X是Y」時會問「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組件來講,輸入是屬性,輸出是元素樹。

返回的元素樹能夠包含描述DOM節點的元素和描述其餘組件的元素。這使你能夠組合相互獨立的UI部分,而不依賴於它們的內部DOM結構。

咱們使用React建立、更新和銷燬實例,用從組件返回的元素來描述它們,而React負責管理這些實例。

組件能夠是類(class)或函數(function)

在上面的代碼中,Form, Message, 和 Button都是React組件。它們既能夠被寫成函數形式就像上面代碼同樣,也能夠寫成類的形式繼承自React.Component。這三種聲明組件的方式是等效的。

// 1) As a function of props
const Button = ({ children, color }) => ({
  type: 'button',
  props: {
    className: 'button button-' + color,
    children: {
      type: 'b',
      props: {
        children: children
      }
    }
  }
});

// 2) Using the React.createClass() factory
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
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
          }
        }
      }
    };
  }
}
複製代碼

當一個組件被定義爲一個類時,它比一個函數組件更強大一些。它能夠存儲一些本地狀態,並在建立或銷燬相應的DOM節點時執行自定義邏輯。

函數組件功能不太強大,可是比較簡單,它就像一個類組件,只有一個render()方法。除非您須要只在類中可用的特性,不然咱們鼓勵您使用函數組件。

然而,不管是函數組件仍是類組件,它們本質上都是React的組件。它們將屬性做爲輸入,並將元素做爲輸出返回。

自上而下的協調算法

當你調用:

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

React將詢問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調用協調算法過程的一部分,它是在調用ReactDOM.render()setState()時被觸發執行。在協調結束時,React知道結果DOM樹,而像react-dom或react-native這樣的渲染器會使用所需的最小變化集合更新到DOM節點(在React Native的狀況下,會應用特定於平臺的視圖)。

這種漸進的解析過程也是React應用程序易於優化的緣由所在。若是組件樹的某些部分太大,以至React沒法有效訪問。若是相關的屬性沒有改變,你能夠告訴它跳過這個「細節」只diffing組件樹的某些部分。若是這些屬性是不可變的,那麼計算它們是否改變是很是快的,因此React和immutability能夠很好地協同工做,而且能夠用最小的努力提供最佳優化。

您可能已經注意到,這篇博客文章主要討論了組件和元素,而不是實例。事實是,與大多數面向對象的UI框架相比,React中的實例的重要性要小得多。

只有聲明爲類的組件纔有實例,並且你永遠不會直接建立它們,React爲你作這些。因爲父組件實例訪問子組件實例機制已經存在,它們只用於必要的操做(例如設置字段的焦點),一般應該避免使用。

React負責爲每一個類組件建立一個實例,所以可使用方法和本地狀態以面向對象的方式編寫組件,但除此以外,實例在React的編程模型中不是很重要,由React本身管理。

總結

元素是一個普通對象,它根據DOM節點或其餘組件描述但願在屏幕上顯示的內容。元素能夠在其屬性中包含其餘元素。建立一個React元素很便宜。一旦建立了一個元素,它就不會發生改變。

組件能夠用幾種不一樣的方式聲明。它能夠是一個帶有render()方法的類。或者在簡單的狀況下,能夠將它定義爲一個函數。在這兩種狀況下,它都接受屬性做爲輸入,並返回一個元素樹做爲輸出。

當一個組件接收到一些屬性做爲輸入時,它就變成了一個特定的父組件,返回一個元素及其類型和這些屬性。這就是爲何人們說這些屬性是單向流動的:從父到子。

實例就是在編寫的組件類中引用爲this的實例。它對於存儲本地狀態和響應生命週期事件很是有用。

函數組件沒有實例。類組件有實例,但你不須要直接建立組件實例—react負責這方面的工做。

最後,建立元素可使用React.createElement(), JSX或者element factory helper.不要在實際代碼中將元素編寫爲普通對象—要知道它們是隱藏在底層的普通對象。

拓展閱讀

出於安全因素,全部的React元素對象須要一個額外的字段 - $$typeof: Symbol.for('react.element')聲明。這是上文中遺漏的地方。本篇文章使用元素的內聯對象來讓你瞭解底層發生了什麼,可是除非你向元素添加$$typeof,或者更改代碼以使用react. createElement()JSX,不然代碼不會按預設的邏輯運行。

相關文章
相關標籤/搜索