React官方定義高階組件的概念是:
A higher-order component is a function that takes a component and returns a new component.
通常情況下郑象,實現(xiàn)高階組件的方式有以下兩種:
屬性代理(Props Proxy)
反向繼承(Inheritance Inversion)
屬性代理
實質上是通過包裹原來的組件來操作props梗摇,舉個簡單的例子:
import React, { Component } from 'React';
//高階組件定義
const HOC = (WrappedComponent) =>
class WrapperComponent extends Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
//普通的組件
class WrappedComponent extends Component{
render(){
//....
}
}
//高階組件使用
export default HOC(WrappedComponent)
我們可以看見函數(shù)HOC返回了新的組件(WrapperComponent),這個組件原封不動的返回作為參數(shù)的組件(也就是被包裹的組件:WrappedComponent)荒吏,并將傳給它的參數(shù)(props)全部傳遞給被包裹的組件(WrappedComponent)闭树。這么看起來好像并沒有什么作用朴爬,其實屬性代理的作用還是非常強大的。
操作props
我們看到之前要傳遞給被包裹組件WrappedComponent的屬性首先傳遞給了高階組件返回的組件(WrapperComponent)惰帽,這樣我們就獲得了props的控制權(這也就是為什么這種方法叫做屬性代理)憨降。我們可以按照需要對傳入的props進行增加、刪除善茎、修改(當然修改帶來的風險需要你自己來控制)券册,舉個例子:
const HOC = (WrappedComponent) =>
class WrapperComponent extends Component {
render() {
const newProps = {
name: 'HOC'
}
return <WrappedComponent
{...this.props}
{...newProps}
/>;
}
}
在上面的例子中,我們?yōu)楸话M件(WrappedComponent)新增加了固定的name屬性垂涯,因此WrappedComponent組件中就會多一個name的屬性烁焙。
抽象state
屬性代理的情況下,我們可以將被包裹組件(WrappedComponent)中的狀態(tài)提到包裹組件中耕赘,一個常見的例子就是實現(xiàn)不受控組件到受控的組件的轉變
class WrappedComponent extends Component {
render() {
return <input name="name" {...this.props.name} />;
}
}
const HOC = (WrappedComponent) =>
class extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
};
this.onNameChange = this.onNameChange.bind(this);
}
onNameChange(event) {
this.setState({
name: event.target.value,
})
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onNameChange,
},
}
return <WrappedComponent {...this.props} {...newProps} />;
}
}
上面的例子中通過高階組件骄蝇,我們將不受控組件(WrappedComponent)成功的轉變?yōu)槭芸亟M件.
用其他元素包裹組件
render(){
<div>
<WrappedComponent {...this.props} />
</div>
}
這種方式將被包裹組件包裹起來,來實現(xiàn)布局或者是樣式的目的操骡。
在屬性代理這種方式實現(xiàn)的高階組件九火,以上述為例赚窃,組件的渲染順序是: 先WrappedComponent再WrapperComponent(執(zhí)行ComponentDidMount的時間)。而卸載的順序是先WrapperComponent再WrappedComponent(執(zhí)行ComponentWillUnmount的時間)岔激。
高階組件的用法勒极,其實就是封裝個函數(shù)將傳入的組件添加上數(shù)據(jù),直接導出即可虑鼎,我們常用的react-redux 中的 connect(Children) 一個道理辱匿,封裝完將數(shù)據(jù)導入到組件當中,組件相應的具有數(shù)據(jù)炫彩,以及具有了dispatch方法匾七,就是這么個封裝。
話不多說直接上個小栗子:
class Parents extends Component {
constructor(props) {
super(props);
this.state = {
parentsSourse: '我是父組件數(shù)據(jù)'
}
}
render() {
<>
<Children />
這是父組件江兢,相當于我們的外層組件
</>
}
}
class Children extends Component {
render() {
<>
這是子組件昨忆,我們展示組件
</>
}
}
我們假如我們想讓父組件包含的組件都具有一個屬性值,這個值是 newType: true, 此時我們可以直接向下級 Childlren 傳遞杉允,那么我們也可以封裝下父組件導出個高階組件,那么這個方法可以這么寫:
const Hoc_component = (HocCompoent) => {
return class NewComponent extends React.Component{
constructor(props){
super(props);
this.state={}
}
render() {
const props = { newType: true }
return <HocCompoent {...this.props} {...props}/>
}
}
}
// 此時所有的組件只要使用
Hoc_component(Children); // 此時的子組件就具有了這個方法包裝的 newType屬性邑贴,我們可以去打印看下。
下面的例子夺颤,我們把兩個組件相似的生命周期方法提取出來痢缎,通過包裝胁勺,能夠節(jié)省非常多的重復代碼世澜。
// CommentList
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// "DataSource" is some global data source
comments: DataSource.getComments()
};
}
componentDidMount() {
// Subscribe to changes
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// Clean up listener
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// Update component state whenever the data source changes
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
// BlogPost
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return <TextBlock text={this.state.blogPost} />;
}
}
他們雖然是兩個不同的組件,對DataSource的需求也不同署穗,但是他們有很多的內容是相似的:
- 在組件渲染之后監(jiān)聽DataSource
- 在監(jiān)聽器里面調用setState
- 在unmout的時候刪除監(jiān)聽器
在大型的工程開發(fā)里面寥裂,這種相似的代碼會經常出現(xiàn),那么如果有辦法把這些相似代碼提取并復用案疲,對工程的可維護性和開發(fā)效率可以帶來明顯的提升封恰。
使用HOC我們可以提供一個方法,并接受不了組件和一些組件間的區(qū)別配置作為參數(shù)褐啡,然后返回一個包裝過的組件作為結果诺舔。
function withSubscription(WrappedComponent, selectData) {
// ...and returns another component...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ... that takes care of the subscription...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... and renders the wrapped component with the fresh data!
// Notice that we pass through any additional props
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
然后我們就可以通過簡單的調用該方法來包裝組件:
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
注意:在HOC中我們并沒有修改輸入的組件,也沒有通過繼承來擴展組件备畦。HOC是通過組合的方式來達到擴展組件的目的低飒,一個HOC應該是一個沒有副作用的方法。