高階組件(HOC)是react中的高級技術,用來重用組件邏輯。但高階組件自己並非React API。它只是一種模式,這種模式是由react自身的組合性質必然產生的。javascript
具體而言,高階組件就是一個函數,且該函數接受一個組件做爲參數,並返回一個新的組件。前端
const EnhancedComponent = higherOrderComponent(WrappedComponent);
複製代碼
一般咱們寫的都是對比組件,那什麼是對比組件呢?對比組件將 props
屬性轉變成 UI,高階組件則是將一個組件轉換成另外一個組件。java
高階組件在 React 第三方庫中很常見,好比 Redux 的 connect
方法和 Relay 的 createContainer
。react
以前用混入(mixins)技術來解決橫切關注點。但是混入(mixins)技術產生的問題要比帶來的價值大。因此就移除混入(mixins)技術,對於如何轉換你已經使用了混入(mixins)技術的組件,可查看更多資料。顯然,橫切關注點就用高階組件(HOC)來解決了。git
1.假設有一個評論組件(CommentList),該組件從外部數據源訂閱數據並渲染github
// CommentList.js
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> ); } } 複製代碼
2.而後,有一個訂閱單個博客文章的組件(BlogPost)算法
// BlogPost.js
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
}
}
conponentDidMount() {
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} />; } } 複製代碼
3.以上,評論組件 CommentList
和 文章訂閱組件 BlogPost
有如下不一樣數組
DataSource
的方法不一樣但是,它們也有相同點app
DataSource
添加一個改變的監聽器;setState
;一個大型應用中,從 DataSource
訂閱數據並調用 setState
的模式會屢次使用,這個時候做爲前端就會嗅出代碼要整理一下,可以抽出相同的地方做爲一個抽象,而後許多組件可共享它,這就是高階組件產生的背景。函數
4.咱們使用個函數 withSubscription
讓它完成如下功能
const CommentListWithSubscription = withSubscription(
CommentList,
DataSource => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
複製代碼
上面函數 withSubscription
的第一個參數是咱們以前寫的兩個組件,第二個參數檢索所須要的數據(DataSource 和 props)。
那這個函數 withSubscription
該怎麼寫呢?
const withSubscription = (TargetComponent, selectData) => {
return class extends React.Component {
constructor(props){
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount(){
DataSource.addChangeListener(this.handleChange);
}
componentDidMount(){
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render(){
return <TargetComponent data={this.state.data} {...this.props} /> } } } 複製代碼
5.總結下
props
屬性以及新的數據 data
用於渲染輸出。高階組件並不關心數據的使用方式,被包裹組件不關心數據來源。props
屬性。這就是能夠替換另外一個高階組件,只要他們提供相同的 props
屬性給被包裹組件便可。你能夠把高階組件當成一套主題皮膚。如今,咱們對高階組件已經有了初步認識,但是實際業務當中,咱們寫高階組件時,容易寫着寫着就修改了組件的內容,千萬要抵住誘惑。好比
const logProps = (WrappedComponent) => {
WrappedComponent.prototype.componentWillReceiveProps = function(nextProps) {
console.log('CurrentProps', this.props);
console.log('NextProps', nextProps);
}
return WrappedComponent;
}
const EnhancedComponent = logProps(WrappedComponent);
複製代碼
logProps
有幾個問題WrappedComponent
不能獨立於加強型組件(enhanced component)被重用。EnhancedComponent
上應用另外一個高階組件 logProps2
,一樣也會改去改變 componentWillReceiveProps
,高階組件 logProps
的功能就會被覆蓋。const logProps = (WrappedComponent) => {
return class extends React.Component {
componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
render() {
// 用容器包裹輸入組件,不要修改它,漂亮!
return <WrappedComponent {...this.props} />; } } } 複製代碼
不知道你發現沒有,高階組件和容器組件模式有相同之處。
props
屬性給子組件;高階組件返回的那個組件與被包裹的組件具備相似的接口。
render(){
// 過濾掉專用於這個高階組件的 props 屬性,丟棄 extraProps
const { extraProps, ...restProps } = this.props;
// 向被包裹的組件注入 injectedProps 屬性,這些通常都是狀態值或實例方法
const injectedProps = {
// someStateOrInstanceMethod
};
return (
<WrappedComponent injectedProps={injectedProps} {...restProps} /> ) } 複製代碼
約定幫助確保高階組件最大程度的靈活性和可重用性。
並非全部的高階組件看起來都是同樣的。有時,它們僅接收單獨一個參數,即被包裹的組件:
const NavbarWithRouter = withRouter(Navbar);
複製代碼
通常而言,高階組件會接收額外的參數。在下面這個來自 Relay 的示例中,一個 config
對象用於指定組件的數據依賴:
const CommentWithRelay = Relay.createContainer(Comment, config);
複製代碼
高階組件最多見簽名以下所示:
const ConnectedComment = connect(commentSelector, commentActions)(Comment);
複製代碼
能夠這麼理解
// connect 返回一個函數(高階組件)
const enhanced = connect(commentSelector, commentActions);
const ConnectedComment = enhanced(Comment);
複製代碼
換句話說,connect
是一個返回高階組件的高階函數!可是這種形式多少讓人有點迷惑,可是它有一個性質,只有一個參數的高階函數(connect
函數返回的),返回是 Component => Component
,這樣就可讓輸入和輸出類型相同的函數組合在一塊兒,在一塊兒,在一塊兒
// 反模式
const EnhancedComponent = withRouter(connect(commentSelector, commentActions)(Comment));
// 正確模式
// 你可使用一個函數組合工具
// compose(f, g, h) 和 (...args) => f(g(h(...args)))是同樣的
const enhanced = compose(
withRouter,
connect(commentSelector, commentActions)
);
const EnhancedComponent = enhanced(Comment);
複製代碼
包括 lodash(好比說 lodash.flowRight
), Redux 和 Ramda 在內的許多第三方庫都提供了相似 compose
功能的函數。
若是你的高階組件名字是 withSubscription
,且被包裹的組件的顯示名字是 CommentList
,那麼就是用 WithSubscription(CommentList)
這樣的顯示名字:
const withSubscription = (WrappedComponent) => {
// return class extends React.Component { /* ... */ };
class WithSubscription extends React.Component { /* ... */ };
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`
return WithSubscription;
}
const getDisplayName = (WrappedComponent) => {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
複製代碼
**React的差分算法(稱爲協調)**使用組件標識肯定是否更新現有的子樹或扔掉它並從新掛載一個新的。若是 render
方法返回的組件和前一次渲染返回的組件是徹底相同的(===),React就遞歸地更新子樹,這是經過差分它和新的那個完成。若是它們不相等,前一個子樹被徹底卸載掉。
通常而言,你不須要考慮差分算法的原理。可是它和高階函數有關。由於它意味着你不能在組件的 render
方法以內應用高階函數到組件:
render() {
// 每一次渲染,都會建立一個新的EnhancedComponent版本
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 就會引發每一次都會使子對象樹徹底被卸載/從新加載
return <EnhancedComponent />; } 複製代碼
上面代碼會致使的問題
問題:當你應用一個高階組件到一個組件時,儘管,原始組件被包裹於一個容器組件內,也就意味着新組件會沒有原始組件的任何靜態方法。
// 定義靜態方法
WrappedComponent.staticMethod = function() {/*...*/}
// 使用高階組件
const EnhancedComponent = enhance(WrappedComponent);
// 加強型組件沒有靜態方法
typeof EnhancedComponent.staticMethod === 'undefined' // true
複製代碼
解決方案: (1)能夠將原始組件的方法拷貝給容器
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// 必須得知道要拷貝的方法 :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
複製代碼
(2)這樣作,就須要你清楚的知道都有哪些靜態方法須要拷貝。你可使用 hoist-non-react-statics 來幫你自動處理,它會自動拷貝全部非React的靜態方法:
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
複製代碼
(3)另一個可能的解決方案就是分別導出組件自身的靜態方法。
// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...export the method separately...
export { someFunction };
// ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';
複製代碼
refs
屬性不能貫穿傳遞通常來講,高階組件能夠傳遞全部的 props
屬性給包裹的組件,可是不能傳遞 refs
引用。由於並非像 key
同樣,refs
是一個僞屬性,React 對它進行了特殊處理。若是你向一個由高階組件建立的組件的元素添加 ref
應用,那麼 ref
指向的是最外層容器組件實例的,而不是被包裹的組件。
React16版本提供了 React.forwardRef
的 API 來解決這一問題,可在 refs
傳遞章節中瞭解下。