上一篇介紹了三種設計模式,包括:
-
容器與展示組件;
-
高階組件;
-
render props。
這篇我們繼續介紹三種設計模式,包括:
-
context模式 ;
-
組合組件 ;
-
繼承模式。
爲了更好的理解,你可以將相應源碼下載下來查看:https://github.com/imalextu/learn-react-patterns
一、Context模式
>>>>
概念介紹
React 的 Context 接口提供了一個無需爲每層組件手動添加 props ,就能在組件樹間進行數據傳遞的方法。
在一個典型的 React 應用中,數據是通過 props 屬性自上而下(由父及子)進行傳遞的,但這種做法對於某些類型的屬性而言是極其繁瑣的(例如:地區偏好,UI 主題),這些屬性是應用程序中許多組件都需要的。Context 提供了一種在組件之間共享此類值的方式,而不必顯式地通過組件樹的逐層傳遞 props。
>>>>
示例
React v16.3.0前後的Context相關API不同,這邊只介紹新版本的Context使用方法。
第一步:新建createContext
首先,要用新提供的 createContext 函數創造一個“上下文”對象。
const ThemeContext = React.createContext();
第二步:生成Provider 和 Consumer
接着,我們用ThemeContext生成兩個屬性,分別是Provider和Consumer。從字面意思即可理解。Provider供數據提供者使用,Consumer供數據消費者使用。
const ThemeProvider = ThemeContext.Provider;
const ThemeConsumer = ThemeContext.Consumer;
第三步:使用ThemeProvider給數據提供者
const Context = () => {
return (
<div>
<ThemeProvider value={{ mainColor: 'blue', textColor: 'pink' }} >
<Page />
</ThemeProvider>
</div>
)
}
// 調用context
const Page = () => (
<div>
<Title>標題</Title>
<Content>
內容
</Content>
</div>
);
第四步:使用ThemeConsumer給數據接收者
// 這裏演示一個class組件。Counsumer使用了renderProps模式哦。
class Title extends React.Component {
render() {
return (
<ThemeConsumer>
{
(theme) => (
<h1 style={{ color: theme.mainColor }}>
{this.props.children}
</h1>
)
}
</ThemeConsumer>
);
}
}
// 這裏演示一個函數式組件
const Content = (props, context) => {
return (
<ThemeConsumer>
{
(theme) => (
<p style={{ color: theme.textColor }}>
{props.children}
</p>
)
}
</ThemeConsumer>
);
};
>>>>
模式所解決的問題
Context 主要應用場景在於很多不同層級的組件需要訪問同樣一些的數據。如下圖,組件a、組件g、組件f需要共享數據,則只需要在最外層套上Provider,需要共享的組件使用Consumer即可。
>>>>
使用注意事項
因爲 context 會使用參考標識(reference identity)來決定何時進行渲染,這裏可能會有一些陷阱,當 provider 的父組件進行重渲染時,可能會在 consumers 組件中觸發意外的渲染。舉個例子,當每一次 Provider 重渲染時,以下的代碼會重渲染所有下面的 consumers 組件,因爲 value 屬性總是被賦值爲新的對象:
class App extends React.Component {
render() {
return (
<Provider value={{something: 'something'}}>
<Toolbar />
</Provider>
);
}
}
爲了防止這種情況,將 value 狀態提升到父節點的 state 裏:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
二、組合組件
>>>>
概念介紹
Compound Component 翻譯爲組合組件。借用組合組件,使用者只需要傳遞子組件,子組件所需要的props在父組件會封裝好,引用子組件的時候就沒必要傳遞所有props了。組合組件核心的兩個方法是React.Children.map和React.cloneElement。React.Children.map 用來遍歷獲得組件的子元素。React.cloneElement 則用來複制元素,這個函數第一個參數就是被複制的元素,第二個參數可以增加新產生元素的 props ,我們就是利用這個函數,把想要的 props 傳入子元素。>>>>
示例
我們設計一個類似於 antd 中的 Tabs 組件,提供tab切換功能,而被選中的TabItem需要高亮。
如果我們使用常規寫法,用 Tabs 中一個 state 記錄當前被選中的 Tabitem 序號,然後根據這個 state 傳遞 props 給 TabItem,還需要傳遞一個 onClick 事件進去,捕獲點擊選擇事件。
<TabItem active={true} onClick={this.onClick}>One</TabItem>
<TabItem active={false} onClick={this.onClick}>Two</TabItem>
<TabItem active={false} onClick={this.onClick}>Three</TabItem>
每次增加一個TabItem,是不是都需要傳遞active和onClick,這太繁瑣了!我們用compound模式解決這個問題。
const TabItem = (props) => {
const {active, onClick} = props;
const tabStyle = {
'max-width': '150px',
color: active ? 'red' : 'green',
border: active ? '1px red solid' : '0px',
};
return (
<h1 style={tabStyle} onClick={onClick}>
{props.children}
</h1>
);
};
// jsx調用Tabs以及TabItem
const Compound = (props, context) => {
return (
<Tabs>
<TabItem>One</TabItem>
<TabItem>Two</TabItem>
<TabItem>Three</TabItem>
<TabItem>Four</TabItem>
</Tabs>
);
};
上面的代碼展示了我們最終調用Tabs以及TabItem的樣子。重點在於Tabs我們要如何實現:
class Tabs extends React.Component {
state = {
activeIndex: 0
}
render() {
const newChildren = React.Children.map(this.props.children, (child, index) => {
if (child.type) {
return React.cloneElement(child, {
active: this.state.activeIndex === index,
onClick: () => this.setState({activeIndex: index})
});
} else {
return child;
}
});
return (
<Fragment>
{newChildren}
</Fragment>
);
}
}
原本我們要如此調用:
<TabItem active={false} onClick={this.onClick}>One</TabItem>
現在我們這樣調用就可以了:
<TabItem>One</TabItem>
通過組合使用React.Children.map和React.cloneElement,我們讓TabItem獲得了它想要的屬性,簡化了TabItem的使用,是不是很神奇?>>>>
模式所解決的問題
組合組件設計模式一般應用在一些共享組件上。如 select 和 option , Tab 和TabItem 等,通過組合組件,使用者只需要傳遞子組件,子組件所需要的 props 在父組件會封裝好,引用子組件的時候就沒必要傳遞所有 props 了。我們可以在共享的組件中運用這種模式,簡化組件使用者的調用方式,antd 當中你就能看到許多組合組件的使用。
三、繼承模式
>>>>
概念介紹
說了那麼多的模式,我們最後來談談很熟悉的繼承模式。如果組件定義爲class組件,那麼我們當然可以使用繼承的模式來實現組件的複用。>>>>
示例
我們通過一個基類來實現一些通用的邏輯,然後再通過繼承分別實現兩個子類。
class Base extends React.PureComponent {
getAlbumItem = () => {
return null
}
render () {
return (
<div style={{border:'1px solid red',margin:5,width:300}}>
{this.getAlbumItem()}
<div>通用邏輯寫這裏</div>
</div>
)
}
}
class Mobile extends Base {
getAlbumItem = () => {
return <span>mobile</span>
}
}
class Pc extends Base {
getAlbumItem = () => {
return <span>pc</span>
}
}
我們具體看下Provider組件是如何定義的。通過這段代碼props.children(allProps),我們調用了傳入的函數。
const Provider = (props) => {
// 判斷是否是女性用戶
let isWoman = Math.random() > 0.5 ? true : false
if (isWoman) {
const allProps = { add: '高階組件增加的屬性', ...props }
return props.children(allProps)
} else {
return <div>女士專用,男士無權瀏覽</div>;
}
}
我們可以看到Mobile和Pc共享了Base的邏輯,實現了複用。>>>>
組合與繼承
如果你剛使用React,可能繼承的方式對大家來說更熟悉的。因爲繼承看起來很方便,也很好理解。但是React官方並不推薦使用繼承,因爲各種組合的模式完全足夠使用,上面的例子我們完全可以用組合的思想去實現。爲什麼不推薦使用繼承?繼承有兩個缺點,其一是,父類的屬性和方法,子類是無條件繼承的。也就是說,不管子類願意不願意,都必須繼承父類所有的屬性和方法,這樣就不夠靈活了。其二是,js中class並不直接支持多繼承。這兩個缺點使得繼承相對於組合組件缺少了靈活性以及可擴展性。請記住,組合優於繼承!組件的複用請第一時間想到使用組合而非繼承。
尾聲
到這裏,六種React組件設計模式就就講完了。這六種模式已經覆蓋了絕大多數的組件使用場景。隨着React的更新,也許將來會有更多組件設計模式出現。但是思想都是想通的,比如“責任分離”、“不要重複自己”(DRY,Don't Repeat Yourself) 等等。明白這些代碼設計思想,將來我們也能很快地掌握新的組件設計模式。
參考文檔:
-
React官方文檔
(http://t.cn/AiYGz4Na)
-
React Component Patterns
(http://t.cn/EvsJ8gj)
-
React實戰:設計模式和最佳實踐
(http://t.cn/EUy09Ml)
-
Presentational and Container Components
(http://t.cn/RqMyfwV)
-
React組件「設計模式」快速指南
http://t.cn/AiThDOqG
-
爲什麼老鳥要告訴你優先使用組合而不是繼承?
http://t.cn/AiThD8E4