複用是元件化開發體系的立命之本,可以說元件化的初衷就是為了複用性。但是元件化的複用方式也存在一定的問題,其中拆分粒度就是其中一個繞不開的話題,今天我們們就來講一講 React 當中的一個不太常用的 API:
cloneElement
,他如何幫組我們更好得進行元件拆分。
假如我們有一個Layout
元件,那麼一般來說這個元件主要接收的就是children
,把它放在主要內容的部分,然後元件本身的節點來控制佈局,那麼這個時候如果我們這個佈局包含兩個部分呢,比如還有一個header
部分,是跟主要內容有明顯區分的。
比如:
那麼我們這個時候會如何設計這個元件呢?
版本一
function Layout({ header: Header, children }) {
return (
<div className='container'>
<div className='header'>
<Header />
</div>
<div classNmae='content'>{children}</div>
</div>
)
}
複製程式碼
這應該是我們比較常見的方式,我們通過把具體元件作為Layout
的props
傳入進來,然後按照元件的寫法把它寫入到元件渲染內容之中。
我們想要使用這個元件,一般會像下面這樣:
function Header() {
return <h1>Title Here</h1>
}
;<Layout header={Header}>
<div>content here</div>
</Layout>
複製程式碼
那麼這樣做有什麼問題呢?顯然是有的,最明顯的就是無法在使用Header
的時候指定props
如果Header
有props
,那麼就我們只能硬編碼在Layout
裡面,不能在使用Header
元件的地方進行宣告,所以如果我們想要複用一個Header
元件,我們可能需要再宣告一個元件,比如我們給Header
元件一個叫做message
的prop
用來指定顯示的文字內容
function Header({ message = 'Title Here' }) {
return <h1>{message}</h1>
}
複製程式碼
那麼如果我們想要在不同頁面複用這個元件並且顯示不同的標題,我們需要這麼做:
function BigHeader() {
return <Header message='The Other Title' />
}
複製程式碼
這麼做顯然在元件較為複雜而且props
較多的情況下,也可以達到一定的複用效果,但是追求極致的我們肯定不希望僅僅侷限於此。
第二版
那麼有沒有辦法讓我們可以在使用時能指定props
呢?答案肯定是有的,我們可以將Layout
的header
這個prop
接收的不是元件本體,而是具體的ReactElement
。
function Layout({ header, children }) {
return (
<div className='container'>
<div className='header'>{header}</div>
<div classNmae='content'>{children}</div>
</div>
)
}
複製程式碼
那麼我們在使用的時候就可以非常方便得指定props
<Layout header={<Header message='The Other Title' />}>
<div>Content Here</div>
</Layout>
複製程式碼
要理解我們可以這麼做,首先我們需要弄清楚什麼是ReactElement
。因為我們大部分時候寫React
元件的時候用的都是JSX
,所以很多同學可能並不知道ReactElement
的存在。
其實JSX
經過babel
翻譯之後得到的是如下程式碼:
// jsx
;<div id='id'>content</div>
// js
React.createElement('div', { id: 'id' }, 'content')
複製程式碼
這個函式接收三個引數
component
具體渲染的元件,包括原生 dom 節點(string
)和自定義元件(object
)config
,包括所有props
再加上key
和ref
形成的字典物件children
,子節點內容,可以是ReactElement
、Array
、string
等內容
最後他返回的是一個叫做ReactElement
型別的物件,他會包含後續 React 渲染過程中需要用到的一個節點包含的所有資訊,我們的props.children
其實就是最典型的ReactElement
。
所以在上訴例子中,我們傳入的header
就是一個ReactElement
,所以可以直接作為其他節點的children
而使用。
同時使用這種方式我們還獲得來一個非常大的優勢,那就是我們甚至可以重新定義一個元件,就可以直接使用Layout
。
<Layout header={<h1>The Other Title</h1>}>
<div>Content Here</div>
</Layout>
複製程式碼
這樣同樣也是可以行得通的。
那麼是否到這裡我們就大功告成來呢?NO,NO,NO,我們還是有值得優化的地方。
第三版
試想一下,如果我們的Layout
中接收來header
是一個節點,但是呢他希望對傳入的元件的一些props
有強制的要求呢?比如我們的Header
元件如果還有另外一個prop
叫color
,用來指定文字內容的顯示顏色:
function Header({ message = 'Title Here', color = 'red' }) {
return <h1 style={{ color }}>{message}</h1>
}
複製程式碼
而Layout
要求所有傳入的Header
必須顏色是green
,顯示我們也可以在使用Header
元件的時候自己指定這個prop
,但是如果我們需要強制指定的prop
很多,而且使用Layout
的地方也很多,那麼明顯我們會寫很多重複程式碼,而且如果後面我們需要修改這個要求的時候也會導致多次修改,甚至有些地方忘了修改而導致 bug。那麼這時候我們該怎麼做呢?
我們可以使用一個 API,這個 API 並不常用,但是在這種場景下,他卻非常有用,這就是React.cloneElement
,我們來修改一下Layout
function Layout({ header, children }) {
return (
<div className='container'>
<div className='header'>
{React.cloneElement(header, { color: 'green' })}
</div>
<div classNmae='content'>{children}</div>
</div>
)
}
複製程式碼
通過這樣,我們真正渲染出來的Header
他的props.color
就永遠都是green
。那麼這個 API 是啥意思呢?
顧名思義,他是用來克隆一個ReactElement
,他接收三個引數,第一個是目標element
,第二個是props
,第三個是children
。可見他跟createElement
非常像,唯一的區別是第一個引數從元件變成來節點。
他做的事情其實就是拷貝目標element
,並把後面兩個引數覆蓋原element
的props
,以此建立一個新的ReactElement
。
那麼到此,我們的優化過程也差不多來,當然 demo 顯然是非常簡單的程式碼,現實中的問題往往要複雜很多,比如接收的如果不是一個ReactElement
而是陣列,字串該如何處理。那麼這些問題在這裡就不再繼續深入來,留給各位小夥伴自己去思考吧,畢竟萬變不離其宗,知道了核心思路之後,其他問題也就可以迎刃而解來。