React 組件炬藤、元素以及實例[譯]

注:英文術(shù)語首次出現(xiàn)會有其對應(yīng)中文翻譯的名稱额港,后文將只用中文譯名表述。
React 初學(xué)者很容易被 Components(組件)歧焦、組件的 instances(實例)和 elements(元素)搞混移斩,為什么要用三個不同的術(shù)語描述 UI 呢?

管理實例

如果你是 React 初學(xué)者绢馍,你很有可能只接觸過組件類和實例向瓷。例如,你會通過創(chuàng)建一個類來聲明一個 Button 組件舰涌,在頁面中可能會有這個組件的多個實例猖任,每一個實例都有自己的 propertities(屬性)和 local state(本地狀態(tài)),這是傳統(tǒng)的面向?qū)ο?UI 編程的做法瓷耙,那為什么要引入元素的概念呢朱躺?

傳統(tǒng)的 UI 模型中,需要開發(fā)者關(guān)注子組件實例的創(chuàng)建和銷毀搁痛。如果一個 Form 組件想渲染一個 Button 組件长搀,它需要創(chuàng)建 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 這樣的庫以面向?qū)ο蟮姆绞綄崿F(xiàn) UI 組合時,就是這么干的彻况。

每一個組件必須保留對其 DOM 節(jié)點和 子組件實例的引用谁尸,并在適當?shù)臅r候創(chuàng)建、更新纽甘、銷毀它們良蛮。代碼的規(guī)模也會隨著組件狀態(tài)復(fù)雜化而增大,并且父組件能夠直接訪問子組件的實例悍赢,導(dǎo)致未來難以對它們解耦背镇。

那 React 又是怎么做的呢?

用元素描繪樹

在 React 代碼中泽裳,元素的引入就是為了解決上述問題瞒斩,元素只是一個用來描述一個組件實例及其 DOM 節(jié)點所需屬性的純對象。它只包含組件類型(例如 Button)涮总、相關(guān)屬性(例如 color)以及內(nèi)部的子元素胸囱。

元素并不是一個真正的實例,而是一種用來告訴 React 你希望哪些東西顯示在頁面中的方式瀑梗。你不能調(diào)用元素里的任何方法烹笔,因為它一個不可變的描述對象裳扯,包含兩個屬性:type(string | ReactClass)props(Object)

當元素的 type 是一個字符串谤职,就代表 DOM 節(jié)點中對應(yīng)的標簽名饰豺,props 對應(yīng)標簽上的屬性,這就是 React 將要渲染的內(nèi)容允蜈。

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

這段元素的代碼表示的是下面這樣的HTML:

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

請注意元素是怎么嵌套的冤吨,按照慣例,當我們需要構(gòu)建元素樹的時候饶套,就會在父級元素的 props.children 中將子元素詳列出來漩蟆。

重要的是,不管是子元素還是父級元素妓蛮,它們都只是一個描述性的對象而非真正的實例怠李。它們并沒有引用頁面中的任何標簽。你可以在創(chuàng)建它們之后就丟到一邊蛤克,不會有多大關(guān)系捺癞。

React 元素容易遍歷,不需要被解析构挤,當然也比真正的 DOM 元素更輕量翘簇,因為它們只是普通對象。

組件元素

然而儿倒,元素的 type 也可以是一個表示 React 組件的函數(shù)或者類:

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

這就是 React 的核心思想了版保。

一個描述組件的元素也屬于元素的范疇,就像一個描述 DOM 節(jié)點的元素一樣夫否,它們可以互相嵌套混搭使用

上述這個特點允許你定義一個 DangerButton 組件彻犁,并指定一個 color 屬性,完全不需要擔心 Button 是否引用了一個真實的 DOM 標簽 <button> 或者 <div> 或者別的標簽凰慈。

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

在一棵 DOM 樹中汞幢,可以混合使用匹配 DOM 節(jié)點或者 React 組件的元素。

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>
);

這種混搭模式能保持組件之間相互解耦森篷,因為可以通過組合唯一地表達 is-ahas-a 兩種關(guān)系:

  • Button 是一個有指定屬性的 DOM button 元素
  • DangerButton 是一個有指定屬性的 Button 元素
  • DeleteAccount 在一個 <div> 內(nèi)包含了 ButtonDangerButton 元素

組件封裝元素樹

當 React 看見一個元素的類型是函數(shù)或者類的時候,它知道去問那個組件會渲染出什么元素豺型,并安排好對應(yīng)的屬性仲智。

當它看見這樣的元素時:

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

React 將會詢問 Button 渲染出什么元素,然后 Button 就會返回這個元素:

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

React 會不斷重復(fù)這個過程姻氨,直到知道頁面中每一個組件最根本的 DOM 標簽元素為止钓辆。

還記得上面那個 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 節(jié)點的元素和描述其它組件的元素啸臀。這允許你獨立組裝部分 UI 而毋需依賴于它們的內(nèi)部 DOM 結(jié)構(gòu)

我們讓 React 負責創(chuàng)建、更新烁落、銷毀實例乘粒,我們只負責描述它們,React 負責管理實例顽馋。

組件可以是類或者函數(shù)

在上面的代碼中,Form幌羞,MessageButton 都是 React 組件寸谜,它們可以寫成函數(shù)的形式,也可以是繼承于 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
          }
        }
      }
    };
  }
}

當一個組件被聲明為一個類時熊痴,它比函數(shù)式組件稍微強大一些,因為可以保存一些本地狀態(tài)以及在生命周期函數(shù)內(nèi)執(zhí)行自定義邏輯等聂宾。

函數(shù)式組件沒那么強大果善,但勝在簡單,它就像一個只有 render() 方法的組件類系谐。除非你需要那些類才能提供的特性巾陕,否則用函數(shù)式組件就好了。

自頂向下的調(diào)度

當你這樣調(diào)用:

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 稱為調(diào)度茶袒,這個過程始于你調(diào)用 ReactDOM.render() 或者 setState() 梯刚。在調(diào)度結(jié)束時,React 就能獲得整棵 DOM 樹薪寓,然后 react-don 或者 react-native 這樣的渲染器將在需要更新的時候執(zhí)行最少的 DOM 操作亡资。

這個逐步提煉的過程正是 React APP 易于優(yōu)化的原因,如果組件樹的某些部分規(guī)模太大向叉,React 訪問效率不高锥腻,那么你可以告訴 React 如果屬性沒有變化就略過這部分組件樹的提煉操作。當屬性是不可變的數(shù)據(jù)時母谎,可以很快判斷它是否發(fā)生變化旷太。因此,React 和不可變數(shù)據(jù)是一對天生的好基友,對于優(yōu)化性能有事半功倍的效果供璧。

你可能發(fā)現(xiàn)這篇文章花了很多篇幅介紹組件和元素存崖,但實例卻沒怎么提起,實際上睡毒,實例在 React 中的作用并沒有像在大部分面向?qū)ο?UI 框架中那么重要来惧。

只有聲明為類的組件才有實例,而且你不用直接創(chuàng)建這些實例演顾,React 會幫你搞定供搀。除了一些必要的場景(例如讓某個表單域獲得焦點),一般情況下應(yīng)避免觸碰組件實例钠至。

總結(jié)

元素就是一個用語描述出現(xiàn)在頁面中的 DOM 節(jié)點或者 React 組件的純對象葛虐。元素可以在自己的屬性中包含其它元素。創(chuàng)建一個元素的成本很低棉钧,一旦元素被創(chuàng)建之后屿脐,就不再發(fā)生變化。

React 組件可以用好幾種方式聲明宪卿,可以是一個包含 render() 方法的類的诵,也可以是一個簡單的函數(shù),不管怎么樣佑钾,它都是以屬性作為輸入西疤,返回元素樹作為輸出。

當一個組件被注入一些屬性值時休溶,屬性值來源于它的父級元素代赁,所以人們常說,屬性在 React 中是單向流動的:從父級到子元素兽掰。

所謂的實例管跺,就是你在組件類中用 this 引用的那個對象,對于保存本地狀態(tài)以及介入生命周期函數(shù)是有用的禾进。

函數(shù)式組件沒有實例豁跑,類組件才有,但你從來不需要手動創(chuàng)建泻云,React 會幫你搞定艇拍。

最后,要想創(chuàng)建元素宠纯,可以使用 React.createElement卸夕,JSX 或者 element factory helper,不要在代碼中手動把元素寫成純對象的形式婆瓜,你只要知道它們是純對象就好了快集。

原文:React Components, Elements, and instances

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贡羔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子个初,更是在濱河造成了極大的恐慌乖寒,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件院溺,死亡現(xiàn)場離奇詭異楣嘁,居然都是意外死亡,警方通過查閱死者的電腦和手機珍逸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門逐虚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人谆膳,你說我怎么就攤上這事叭爱。” “怎么了漱病?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵买雾,是天一觀的道長。 經(jīng)常有香客問我缨称,道長凝果,這世上最難降的妖魔是什么祝迂? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任睦尽,我火速辦了婚禮,結(jié)果婚禮上型雳,老公的妹妹穿的比我還像新娘当凡。我一直安慰自己,他們只是感情好纠俭,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布沿量。 她就那樣靜靜地躺著,像睡著了一般冤荆。 火紅的嫁衣襯著肌膚如雪朴则。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天钓简,我揣著相機與錄音乌妒,去河邊找鬼。 笑死外邓,一個胖子當著我的面吹牛撤蚊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播损话,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼侦啸,長吁一口氣:“原來是場噩夢啊……” “哼槽唾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起光涂,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤庞萍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后顶捷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挂绰,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年服赎,在試婚紗的時候發(fā)現(xiàn)自己被綠了葵蒂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡重虑,死狀恐怖践付,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缺厉,我是刑警寧澤永高,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站提针,受9級特大地震影響命爬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辐脖,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一饲宛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嗜价,春花似錦艇抠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至瑟由,卻和暖如春絮重,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背歹苦。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工青伤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人暂氯。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓潮模,卻偏偏與公主長得像,于是被迫代替她去往敵國和親痴施。 傳聞我的和親對象是個殘疾皇子擎厢,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容