本文代碼淺顯易懂,思想深刻實用。此屬於react進階用法,若是你還不瞭解react,建議從文檔開始看起。javascript
咱們都知道高階函數是什麼, 高階組件實際上是差很少的用法,只不過傳入的參數變成了react組件,並返回一個新的組件.html
A higher-order component is a function that takes a component and returns a new component.java
形如:react
const EnhancedComponent = higherOrderComponent(WrappedComponent);複製代碼
高階組件是react應用中很重要的一部分,最大的特色就是重用組件邏輯。它並非由React API定義出來的功能,而是由React的組合特性衍生出來的一種設計模式。若是你用過redux,那你就必定接觸太高階組件,由於react-redux中的connect就是一個高階組件。git
原文github.com/sunyongjian…
歡迎star
另外本次demo代碼都放在 github.com/sunyongjian…
clone下來跑一下加深理解es6
先來一個最簡單的高階組件github
import React, { Component } from 'react';
import simpleHoc from './simple-hoc';
class Usual extends Component {
render() {
console.log(this.props, 'props');
return (
<div> Usual </div>
)
}
}
export default simpleHoc(Usual);複製代碼
import React, { Component } from 'react';
const simpleHoc = WrappedComponent => {
console.log('simpleHoc');
return class extends Component {
render() {
return <WrappedComponent {...this.props}/> } } } export default simpleHoc;複製代碼
組件Usual經過simpleHoc的包裝,打了一個log... 那麼形如simpleHoc就是一個高階組件了,經過接收一個組件class Usual,並返回一個組件class。 其實咱們能夠看到,在這個函數裏,咱們能夠作不少操做。 並且return的組件一樣有本身的生命週期,function,另外,咱們看到也能夠把props傳給WrappedComponent(被包裝的組件)。 高階組件的定義我都是用箭頭函數去寫的,若有不適請參照arrow function算法
高階組件能夠看作是裝飾器模式(Decorator Pattern)在React的實現。即容許向一個現有的對象添加新的功能,同時又不改變其結構,屬於包裝模式(Wrapper Pattern)的一種chrome
ES7中添加了一個decorator的屬性,使用@符表示,能夠更精簡的書寫。那上面的例子就能夠改爲:編程
import React, { Component } from 'react';
import simpleHoc from './simple-hoc';
@simpleHoc
export default class Usual extends Component {
render() {
return (
<div> Usual </div>
)
}
}複製代碼
是一樣的效果。
固然兼容性是存在問題的,一般都是經過babel去編譯的。 babel提供了plugin,高階組件用的是類裝飾器,因此用transform-decorators-legacy
babel
引入裏咱們寫的最簡單的形式,就是屬性代理(Props Proxy)的形式。經過hoc包裝wrappedComponent,也就是例子中的Usual,原本傳給Usual的props,都在hoc中接受到了,也就是props proxy。 由此咱們能夠作一些操做
操做props
最直觀的就是接受到props,咱們能夠作任何讀取,編輯,刪除的不少自定義操做。包括hoc中定義的自定義事件,均可以經過props再傳下去。
import React, { Component } from 'react';
const propsProxyHoc = WrappedComponent => class extends Component {
handleClick() {
console.log('click');
}
render() {
return (<WrappedComponent {...this.props} handleClick={this.handleClick} />); } }; export default propsProxyHoc;複製代碼
而後咱們的Usual組件render的時候, console.log(this.props)
會獲得handleClick.
refs獲取組件實例
當咱們包裝Usual的時候,想獲取到它的實例怎麼辦,能夠經過引用(ref),在Usual組件掛載的時候,會執行ref的回調函數,在hoc中取到組件的實例。經過打印,能夠看到它的props, state,都是能夠取到的。
import React, { Component } from 'react';
const refHoc = WrappedComponent => class extends Component {
componentDidMount() {
console.log(this.instanceComponent, 'instanceComponent');
}
render() {
return (<WrappedComponent {...this.props} ref={instanceComponent => this.instanceComponent = instanceComponent} />); } }; export default refHoc;複製代碼
抽離state
這裏不是經過ref獲取state, 而是經過 { props, 回調函數 } 傳遞給wrappedComponent組件,經過回調函數獲取state。這裏用的比較多的就是react處理表單的時候。一般react在處理表單的時候,通常使用的是受控組件(文檔),即把input都作成受控的,改變value的時候,用onChange事件同步到state中。固然這種操做經過Container組件也能夠作到,具體的區別放到後面去比較。看一下代碼就知道怎麼回事了:
// 普通組件Login
import React, { Component } from 'react';
import formCreate from './form-create';
@formCreate
export default class Login extends Component {
render() {
return (
<div>
<div>
<label id="username">
帳戶
</label>
<input name="username" {...this.props.getField('username')}/>
</div>
<div>
<label id="password">
密碼
</label>
<input name="password" {...this.props.getField('password')}/>
</div>
<div onClick={this.props.handleSubmit}>提交</div>
<div>other content</div>
</div>
)
}
}複製代碼
//HOC
import React, { Component } from 'react';
const formCreate = WrappedComponent => class extends Component {
constructor() {
super();
this.state = {
fields: {},
}
}
onChange = key => e => {
const { fields } = this.state;
fields[key] = e.target.value;
this.setState({
fields,
})
}
handleSubmit = () => {
console.log(this.state.fields);
}
getField = fieldName => {
return {
onChange: this.onChange(fieldName),
}
}
render() {
const props = {
...this.props,
handleSubmit: this.handleSubmit,
getField: this.getField,
}
return (<WrappedComponent {...props} />); } }; export default formCreate;複製代碼
這裏咱們把state,onChange等方法都放到HOC裏,實際上是聽從的react組件的一種規範,子組件簡單,傻瓜,負責展現,邏輯與操做放到Container。好比說咱們在HOC獲取到用戶名密碼以後,再去作其餘操做,就方便多了,而state,處理函數放到Form組件裏,只會讓Form更加笨重,承擔了本不屬於它的工做,這樣咱們可能其餘地方也須要用到這個組件,可是處理方式稍微不一樣,就很麻煩了。
反向繼承(Inheritance Inversion),簡稱II,原本我是叫繼承反轉的...由於有個模式叫控制反轉嘛...
跟屬性代理的方式不一樣的是,II採用經過 去繼承WrappedComponent,原本是一種嵌套的關係,結果II返回的組件卻繼承了WrappedComponent,這看起來是一種反轉的關係。
經過繼承WrappedComponent,除了一些靜態方法,包括生命週期,state,各類function,咱們均可以獲得。上栗子:
// usual
import React, { Component } from 'react';
import iiHoc from './ii-hoc';
@iiHoc
export default class Usual extends Component {
constructor() {
super();
this.state = {
usual: 'usual',
}
}
componentDidMount() {
console.log('didMount')
}
render() {
return (
<div> Usual </div>
)
}
}複製代碼
//IIHOC
import React from 'react';
const iiHoc = WrappedComponent => class extends WrappedComponent {
render() {
console.log(this.state, 'state');
return super.render();
}
}
export default iiHoc;複製代碼
iiHoc return的組件經過繼承,擁有了Usual的生命週期及屬性,因此didMount會打印,state也經過constructor執行,獲得state.usual。
其實,你還能夠經過II:
這裏HOC裏定義的組件繼承了WrappedComponent的render(渲染),咱們能夠以此進行hijack(劫持),也就是控制它的render函數。栗子:
//hijack-hoc
import React from 'react';
const hijackRenderHoc = config => WrappedComponent => class extends WrappedComponent {
render() {
const { style = {} } = config;
const elementsTree = super.render();
console.log(elementsTree, 'elementsTree');
if (config.type === 'add-style') {
return <div style={{...style}}> {elementsTree} </div>;
}
return elementsTree;
}
};
export default hijackRenderHoc;複製代碼
//usual
@hijackRenderHoc({type: 'add-style', style: { color: 'red'}})
class Usual extends Component {
...
}複製代碼
我這裏經過二階函數,把config參數預製進HOC, 算是一種柯理化的思想。
栗子很簡單,這個hoc就是添加樣式的功能。可是它暴露出來的信息卻很多。首先咱們能夠經過config參數進行邏輯判斷,有條件的渲染,固然這個參數的做用不少,react-redux中的connect不就是傳入了props-key 嘛。再就是咱們還能夠拿到WrappedComponent的元素樹,能夠進行修改操做。最後就是咱們經過div包裹,設置了style。但其實具體如何操做仍是根據業務邏輯去處理的...
另外一種狀況就是以前寫過一個組件A,作完上線,以後產品加了一個新需求,很奇怪要作的組件B跟A幾乎如出一轍,但稍微有區別。那我可能就經過II的方式去繼承以前的組件A,好比它在didMount去fetch請求,須要的數據是同樣的。不一樣的地方我就會放到HOC裏,存儲新的state這樣,再經過劫持渲染,把不一樣的地方,添加的地方進行處理。但其實這算Hack的一種方式,能快速解決問題,也反映了組件設計規劃之初有所不足(緣由比較多)。
Container解決不了的時候甚至不太優雅的時候。其實大部分時候包一層Container組件也能作到差很少的效果,好比操做props,渲染劫持。但其實仍是有很大區別的。好比咱們如今有兩個功能的container,添加樣式和添加處理函數的,對Usual進行包裝。栗子:
//usual
class Usual extends Component {
render() {
console.log(this.props, 'props');
return <div> Usual </div>
}
};
export default Usual;
//console - Object {handleClick: function} "props"複製代碼
import React, { Component } from 'react';
import Usual from './usual';
class StyleContainer extends Component {
render() {
return (<div style={{ color: '#76d0a3' }}> <div>container</div> <Usual {...this.props} /> </div>); } } export default StyleContainer;複製代碼
import React, { Component } from 'react';
import StyleContainer from './container-add-style';
class FuncContainer extends Component {
handleClick() {
console.log('click');
}
render() {
const props = {
...this.props,
handleClick: this.handleClick,
};
return (<StyleContainer {...props} />); } } export default FuncContainer;複製代碼
外層Container必需要引入內層Container,進行包裝,還有props的傳遞,一樣要注意包裝的順序。固然你能夠把全部的處理都放到一個Container裏。那用HOC怎麼處理呢,相信你們有清晰的答案了。
const addFunc = WrappedComponent => class extends Component {
handleClick() {
console.log('click');
}
render() {
const props = {
...this.props,
handleClick: this.handleClick,
};
return <WrappedComponent {...props} />; } };複製代碼
const addStyle = WrappedComponent => class extends Component {
render() {
return (<div style={{ color: '#76d0a3' }}> <WrappedComponent {...this.props} /> </div>); } };複製代碼
const WrappenComponent = addStyle(addFunc(Usual));
class WrappedUsual extends Component {
render() {
console.log(this.props, 'props');
return (<div> <WrappedComponent /> </div>);
}
}複製代碼
顯然HOC是更優雅一些的,每一個HOC都定義本身獨有的處理邏輯,須要的時候只須要去包裝你的組件。相較於Container的方式,HOC耦合性更低,靈活性更高,能夠自由組合,更適合應付複雜的業務。每一個HOC負責獨立的功能,好比可能只是一個Loading的效果,不少列表頁都須要,用HOC只須要包裝一下就能夠了,不須要在每一個組件裏再重寫這部分邏輯了。固然當你的需求很簡單的時候,仍是用Container去自由組合,應用場景須要你清楚。
其實官網有不少,簡單介紹一下。
要給hoc添加class名,便於debugger。我上面的好多栗子組件都沒寫class 名,請不要學我,由於我實在想不出叫什麼名了... 當咱們在chrome裏應用React-Developer-Tools的時候,組件結構能夠一目瞭然,因此DisplayName最好仍是加上。
靜態方法要複製
不管PP仍是II的方式,WrappedComponent的靜態方法都不會複製,若是要用須要咱們單獨複製。
refs不會傳遞。 意思就是HOC裏指定的ref,並不會傳遞到子組件,若是你要使用最好寫回調函數經過props傳下去。
不要在render方法內部使用高階組件。簡單來講react的差分算法會去比較 NowElement === OldElement, 來決定要不要替換這個elementTree。也就是若是你每次返回的結果都不是一個引用,react覺得發生了變化,去更替這個組件會致使以前組件的狀態丟失。
// HOC不要放到render函數裏面
class WrappedUsual extends Component {
render() {
const WrappenComponent = addStyle(addFunc(Usual));
console.log(this.props, 'props');
return (<div> <WrappedComponent /> </div>);
}
}複製代碼
使用compose組合HOC。函數式編程的套路... 例如應用redux中的middleware以加強功能。redux-middleware解析
const addFuncHOC = ...
const addStyleHOC = ...//省略
const compose = (...funcs) => component => {
if (funcs.lenght === 0) {
return component;
}
const last = funcs[funcs.length - 1];
return funcs.reduceRight((res, cur) => cur(res), last(component));
};
const WrappedComponent = compose(addFuncHOC, addStyleHOC)(Usual);複製代碼
關於注意點,官網有所介紹,再也不贅述。連接
高階組件最大的好處就是解耦和靈活性,在react的開發中仍是頗有用的。固然這不多是高階組件的所有用法。掌握了它的一些技巧,還有一些限制,你能夠結合你的應用場景,發散思惟,嘗試一些不一樣的用法。