前言
本文程式碼淺顯易懂,思想深入實用。此屬於react進階用法,如果你還不瞭解react,建議從文件開始看起。
我們都知道高階函式是什麼, 高階元件其實是差不多的用法,只不過傳入的引數變成了react元件,並返回一個新的元件.
A higher-order component is a function that takes a component and returns a new component.
形如:
const EnhancedComponent = higherOrderComponent(WrappedComponent);複製程式碼
高階元件是react應用中很重要的一部分,最大的特點就是重用元件邏輯。它並不是由React API定義出來的功能,而是由React的組合特性衍生出來的一種設計模式。如果你用過redux,那你就一定接觸過高階元件,因為react-redux中的connect就是一個高階元件。
原文github.com/sunyongjian…
歡迎star
另外本次demo程式碼都放在 github.com/sunyongjian…
clone下來跑一下加深理解
引入
先來一個最簡單的高階元件
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)的一種
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。但其實具體如何操作還是根據業務邏輯去處理的...
我的應用場景
- 通常我會通過高階元件去優化之前老專案寫的不好的地方,比如兩個頁面UI幾乎一樣,功能幾乎相同,僅僅幾個操作不太一樣,卻寫了兩個耦合很多的頁面級元件。當我去維護它的時候,由於它的耦合性過多,經常會新增一個功能(這兩個元件都要新增),我要去改完第一個的時候,還要改第二個。而且有時候由於我的記性不好,會忘掉第二個... 就會出現bug再返工。更重要的是由於個人比較懶,不想去重構這部分的程式碼,因為東西太多了,花費太多時間。所以加新功能的時候,我會寫一個高階元件,往HOC裡新增方法,把那兩個元件包裝一下,也就是屬性代理。這樣新程式碼就不會再出現耦合,舊的邏輯並不會改變,說不定哪天心情好就會抽離一部分功能到HOC裡,直到理想的狀態。
另一種情況就是之前寫過一個元件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的開發中還是很有用的。
當然這不可能是高階元件的全部用法。掌握了它的一些技巧,還有一些限制,你可以結合你的應用場景,發散思維,嘗試一些不同的用法。