作者: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: Object
1
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
,Message
和Button
都是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物件存在就好。