React元件/元素與例項分析

Jogis發表於2015-12-20

作者:Dan Abramov
譯者:Jogis
譯文連結:https://github.com/yesvods/Blog/issues/5
轉載請註明譯文連結以及譯者資訊

前言

很多React新手對Components以及他們的instances和elements之間的區別感到非常困惑,為什麼要用三種不同的術語來代表那些被渲染在熒屏上的內容呢?

親自管理例項(Managing the Instances)

如果是剛入門React,那麼你應該只是接觸過一些元件類(component classes)以及例項(instances)。打比方,你可能通過class關鍵字宣告瞭一個Button元件。這個程式執行時候,可能會有幾個Button元件的例項(instances)執行在瀏覽器上,每一個例項會有各自的引數(properties)以及本地狀態(state)。這種屬於傳統的物件導向UI程式設計。那麼為什麼會有元素(elements)出現呢?

在這種傳統UI模式上,你需要負責建立和刪除例項(instances)的子元件例項。如果一個Form的元件想要渲染一個Button子元件,需要例項化這個Button子元件,並且手動更新他們的內容。

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又有什麼不同呢?

用元素來描述節點樹(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去修改上面的程式碼,否則那些程式碼並不能正常執行。

相關文章