React官方定義高階組件的概念是:
A higher-order component is a function that takes a component and returns a new component.
通常情況下,實現(xiàn)高階組件的方式有以下兩種:
屬性代理(Props Proxy)
反向繼承(Inheritance Inversion)
屬性代理
實質(zhì)上是通過包裹原來的組件來操作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的控制權(quán)(這也就是為什么這種方法叫做屬性代理)。我們可以按照需要對傳入的props進(jìn)行增加、刪除、修改(當(dāng)然修改帶來的風(fēng)險需要你自己來控制),舉個例子:
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)不受控組件到受控的組件的轉(zhuǎ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)成功的轉(zhuǎn)變?yōu)槭芸亟M件.
用其他元素包裹組件
render(){
<div>
<WrappedComponent {...this.props} />
</div>
}
這種方式將被包裹組件包裹起來,來實現(xiàn)布局或者是樣式的目的。
在屬性代理這種方式實現(xiàn)的高階組件,以上述為例,組件的渲染順序是: 先WrappedComponent再WrapperComponent(執(zhí)行ComponentDidMount的時間)。而卸載的順序是先WrapperComponent再WrappedComponent(執(zhí)行ComponentWillUnmount的時間)。
高階組件的用法,其實就是封裝個函數(shù)將傳入的組件添加上數(shù)據(jù),直接導(dǎo)出即可,我們常用的react-redux 中的 connect(Children) 一個道理,封裝完將數(shù)據(jù)導(dǎo)入到組件當(dāng)中,組件相應(yīng)的具有數(shù)據(jù),以及具有了dispatch方法,就是這么個封裝。
話不多說直接上個小栗子:
class Parents extends Component {
constructor(props) {
super(props);
this.state = {
parentsSourse: '我是父組件數(shù)據(jù)'
}
}
render() {
<>
<Children />
這是父組件,相當(dāng)于我們的外層組件
</>
}
}
class Children extends Component {
render() {
<>
這是子組件,我們展示組件
</>
}
}
我們假如我們想讓父組件包含的組件都具有一個屬性值,這個值是 newType: true, 此時我們可以直接向下級 Childlren 傳遞,那么我們也可以封裝下父組件導(dǎo)出個高階組件,那么這個方法可以這么寫:
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é)省非常多的重復(fù)代碼。
// 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的需求也不同,但是他們有很多的內(nèi)容是相似的:
- 在組件渲染之后監(jiān)聽DataSource
- 在監(jiān)聽器里面調(diào)用setState
- 在unmout的時候刪除監(jiān)聽器
在大型的工程開發(fā)里面,這種相似的代碼會經(jīng)常出現(xiàn),那么如果有辦法把這些相似代碼提取并復(fù)用,對工程的可維護(hù)性和開發(fā)效率可以帶來明顯的提升。
使用HOC我們可以提供一個方法,并接受不了組件和一些組件間的區(qū)別配置作為參數(shù),然后返回一個包裝過的組件作為結(jié)果。
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} />;
}
};
}
然后我們就可以通過簡單的調(diào)用該方法來包裝組件:
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
注意:在HOC中我們并沒有修改輸入的組件,也沒有通過繼承來擴展組件。HOC是通過組合的方式來達(dá)到擴展組件的目的,一個HOC應(yīng)該是一個沒有副作用的方法。