React中的高階元件(HOC)
簡要介紹:React中不會用到元件的繼承,作者選擇用組合來代替繼承,
但是存在一種情況,就是兩種元件方法類似,如果能有一種“類繼承”的
方式,在同一個函式中可以生產這兩種元件,那麼就可以大量的減少程式碼
的冗餘,在React的高階元件實現了Decorator 模式來模擬繼承或者說來
代替繼承。
1、什麼是高階元件(high order component)
高階元件的定義:就是一個函式,這個函式傳入一個元件,然後返回一個新的元件。
高階元件在react的一些外掛中大量使用,比如react-redux中的connect方法,就是接受了一個展示元件,輸出一個容器元件。
2、為什麼要使用高階元件
主要原因:為了程式碼的複用性,減少程式碼的冗餘
下面來具體闡述為什麼需要使用高階元件(以官網文件的例子為例):
(1)CommentList元件
class CommentList extends React.Component {
constructor() {
super();
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>
);
}
}
這個元件很簡單,就是從遠端獲取資料展示一系列的comment,並且監聽資料是否發生變化。
(2)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} />;
}
}
我們來看BlogPost元件,幾乎與comment元件一樣,也是從遠端獲取資料然後展示TextBlock列表。
(3)比較BlogPost和Comment元件
我們發現,這倆個元件的在生命週期中很多函式是相同的,僅僅是在render返回的子元件型別不同。它們又是不同的,比如BlogPost呼叫的是DataSource.getBlogPost方法而Comment呼叫的是DataSource.getComments方法。
缺點:如果在某個場景下,需要成千上百個這種相似元件,那麼我們可能要手工生成這麼成千上百個元件,程式碼的冗餘程度可想而知。
(4)解決方法
如果我們定義一個函式,引數是元件和函式,生成了不同的元件(不同而又相似)。那麼這個函式就是類似於一個工廠的形式,可以批量的產生各種相似的元件,函式類似於下面這種形式。
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
具體的函式為:
// This function takes a component...
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} />;
}
};
}
3、高階元件中的注意事項
(1)首先因為高階元件是一個函式,因此你可以傳遞任意的引數。
(2)不要修改原始元件的方法
(3)不要在render函式中使用HOC:
render() {
// A new version of EnhancedComponent is created on every render
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// That causes the entire subtree to unmount/remount each time!
return <EnhancedComponent />;
}
相當於重新定義和使用了元件,render方法會全部重新執行,這樣就無法利用react的diff演算法,更新式的重新渲染元件。
(4)必須重寫引數元件中的靜態方法
如果不重寫靜態方法,那麼高階元件的返回的那個元件上同名靜態方法不存在,或為undefined
// Define a static method
WrappedComponent.staticMethod = function() {/*...*/}
// Now apply an HOC
const EnhancedComponent = enhance(WrappedComponent);
// The enhanced component has no static method
typeof EnhancedComponent.staticMethod === 'undefined' // true
為了解決上述問題,我們必須將方法拷貝到返回的新的元件中:
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// Must know exactly which method(s) to copy :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
(5)注意ref保留字