注:英文術(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-a
和 has-a
兩種關(guān)系:
-
Button
是一個有指定屬性的 DOMbutton
元素 -
DangerButton
是一個有指定屬性的Button
元素 -
DeleteAccount
在一個<div>
內(nèi)包含了Button
和DangerButton
元素
組件封裝元素樹
當 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
幌羞,Message
和 Button
都是 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
,不要在代碼中手動把元素寫成純對象的形式婆瓜,你只要知道它們是純對象就好了快集。