React進(jìn)階之高階組件
前言
本文代碼淺顯易懂树碱,思想深入實(shí)用。此屬于react進(jìn)階用法变秦,如果你還不了解react成榜,建議從文檔開始看起。
我們都知道高階函數(shù)是什么, 高階組件其實(shí)是差不多的用法蹦玫,只不過傳入的參數(shù)變成了react組件赎婚,并返回一個(gè)新的組件.
A higher-order component is a function that takes a component and returns a new component.
形如:
const EnhancedComponent = higherOrderComponent(WrappedComponent);
高階組件是react應(yīng)用中很重要的一部分,最大的特點(diǎn)就是重用組件邏輯樱溉。它并不是由React API定義出來的功能挣输,而是由React的組合特性衍生出來的一種設(shè)計(jì)模式。如果你用過redux饺窿,那你就一定接觸過高階組件歧焦,因?yàn)閞eact-redux中的connect就是一個(gè)高階組件。
原文https://github.com/sunyongjian/blog/issues/25
歡迎star
另外本次demo代碼都放在 https://github.com/sunyongjian/hoc-demo
clone下來跑一下加深理解
引入
先來一個(gè)最簡(jiǎn)單的高階組件
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的包裝,打了一個(gè)log... 那么形如simpleHoc就是一個(gè)高階組件了绢馍,通過接收一個(gè)組件class Usual向瓷,并返回一個(gè)組件class。 其實(shí)我們可以看到舰涌,在這個(gè)函數(shù)里猖任,我們可以做很多操作。 而且return的組件同樣有自己的生命周期瓷耙,function朱躺,另外,我們看到也可以把props傳給WrappedComponent(被包裝的組件)搁痛。 高階組件的定義我都是用箭頭函數(shù)去寫的长搀,如有不適請(qǐng)參照arrow function
裝飾器模式
高階組件可以看做是裝飾器模式(Decorator Pattern)在React的實(shí)現(xiàn)。即允許向一個(gè)現(xiàn)有的對(duì)象添加新的功能鸡典,同時(shí)又不改變其結(jié)構(gòu)源请,屬于包裝模式(Wrapper Pattern)的一種
ES7中添加了一個(gè)decorator的屬性,使用@符表示彻况,可以更精簡(jiǎn)的書寫谁尸。那上面的例子就可以改成:
import React, { Component } from 'react';
import simpleHoc from './simple-hoc';
@simpleHoc
export default class Usual extends Component {
render() {
return (
<div>
Usual
</div>
)
}
}
是同樣的效果。
當(dāng)然兼容性是存在問題的纽甘,通常都是通過babel去編譯的良蛮。 babel提供了plugin,高階組件用的是類裝飾器悍赢,所以用transform-decorators-legacy
babel
兩種形式
屬性代理
引入里我們寫的最簡(jiǎn)單的形式决瞳,就是屬性代理(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的時(shí)候,
console.log(this.props)
會(huì)得到handleClick. -
refs獲取組件實(shí)例
當(dāng)我們包裝Usual的時(shí)候谤职,想獲取到它的實(shí)例怎么辦,可以通過引用(ref),在Usual組件掛載的時(shí)候亿鲜,會(huì)執(zhí)行ref的回調(diào)函數(shù)允蜈,在hoc中取到組件的實(shí)例冤吨。通過打印,可以看到它的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, 回調(diào)函數(shù) } 傳遞給wrappedComponent組件怠李,通過回調(diào)函數(shù)獲取state。這里用的比較多的就是react處理表單的時(shí)候蛤克。通常react在處理表單的時(shí)候捺癞,一般使用的是受控組件(文檔),即把input都做成受控的构挤,改變value的時(shí)候髓介,用onChange事件同步到state中。當(dāng)然這種操作通過Container組件也可以做到筋现,具體的區(qū)別放到后面去比較版保。看一下代碼就知道怎么回事了:
// 普通組件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里,其實(shí)是遵從的react組件的一種規(guī)范叫胁,子組件簡(jiǎn)單凰慈,傻瓜,負(fù)責(zé)展示驼鹅,邏輯與操作放到Container微谓。比如說我們?cè)贖OC獲取到用戶名密碼之后,再去做其他操作输钩,就方便多了豺型,而state,處理函數(shù)放到Form組件里买乃,只會(huì)讓Form更加笨重姻氨,承擔(dān)了本不屬于它的工作,這樣我們可能其他地方也需要用到這個(gè)組件剪验,但是處理方式稍微不同肴焊,就很麻煩了。
反向繼承
反向繼承(Inheritance Inversion)功戚,簡(jiǎn)稱II娶眷,本來我是叫繼承反轉(zhuǎn)的...因?yàn)橛袀€(gè)模式叫控制反轉(zhuǎn)嘛...
跟屬性代理的方式不同的是,II采用通過 去繼承WrappedComponent啸臀,本來是一種嵌套的關(guān)系届宠,結(jié)果II返回的組件卻繼承了WrappedComponent,這看起來是一種反轉(zhuǎn)的關(guān)系。
通過繼承WrappedComponent豌注,除了一些靜態(tài)方法伤塌,包括生命周期,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會(huì)打印,state也通過constructor執(zhí)行聂宾,得到state.usual果善。
其實(shí),你還可以通過II:
渲染劫持
這里HOC里定義的組件繼承了WrappedComponent的render(渲染)系谐,我們可以以此進(jìn)行hijack(劫持)巾陕,也就是控制它的render函數(shù)。栗子:
//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 {
...
}
我這里通過二階函數(shù)纪他,把config參數(shù)預(yù)制進(jìn)HOC鄙煤, 算是一種柯理化的思想。
栗子很簡(jiǎn)單茶袒,這個(gè)hoc就是添加樣式的功能梯刚。但是它暴露出來的信息卻不少。首先我們可以通過config參數(shù)進(jìn)行邏輯判斷薪寓,有條件的渲染亡资,當(dāng)然這個(gè)參數(shù)的作用很多,react-redux中的connect不就是傳入了props-key 嘛向叉。再就是我們還可以拿到WrappedComponent的元素樹锥腻,可以進(jìn)行修改操作。最后就是我們通過div包裹母谎,設(shè)置了style瘦黑。但其實(shí)具體如何操作還是根據(jù)業(yè)務(wù)邏輯去處理的...
[圖片上傳中...(image-936804-1521617092866-1)]
我的應(yīng)用場(chǎng)景
通常我會(huì)通過高階組件去優(yōu)化之前老項(xiàng)目寫的不好的地方,比如兩個(gè)頁面UI幾乎一樣奇唤,功能幾乎相同供璧,僅僅幾個(gè)操作不太一樣,卻寫了兩個(gè)耦合很多的頁面級(jí)組件冻记。當(dāng)我去維護(hù)它的時(shí)候睡毒,由于它的耦合性過多,經(jīng)常會(huì)添加一個(gè)功能(這兩個(gè)組件都要添加)冗栗,我要去改完第一個(gè)的時(shí)候演顾,還要改第二個(gè)供搀。而且有時(shí)候由于我的記性不好,會(huì)忘掉第二個(gè)... 就會(huì)出現(xiàn)bug再返工钠至。更重要的是由于個(gè)人比較懶葛虐,不想去重構(gòu)這部分的代碼,因?yàn)闁|西太多了棉钧,花費(fèi)太多時(shí)間屿脐。所以加新功能的時(shí)候,我會(huì)寫一個(gè)高階組件宪卿,往HOC里添加方法的诵,把那兩個(gè)組件包裝一下,也就是屬性代理佑钾。這樣新代碼就不會(huì)再出現(xiàn)耦合西疤,舊的邏輯并不會(huì)改變,說不定哪天心情好就會(huì)抽離一部分功能到HOC里休溶,直到理想的狀態(tài)代赁。
另一種情況就是之前寫過一個(gè)組件A,做完上線兽掰,之后產(chǎn)品加了一個(gè)新需求芭碍,很奇怪要做的組件B跟A幾乎一模一樣,但稍微有區(qū)別孽尽。那我可能就通過II的方式去繼承之前的組件A窖壕,比如它在didMount去fetch請(qǐng)求,需要的數(shù)據(jù)是一樣的泻云。不同的地方我就會(huì)放到HOC里,存儲(chǔ)新的state這樣狐蜕,再通過劫持渲染宠纯,把不同的地方,添加的地方進(jìn)行處理层释。但其實(shí)這算Hack的一種方式婆瓜,能快速解決問題,也反映了組件設(shè)計(jì)規(guī)劃之初有所不足(原因比較多)贡羔。
-
Container解決不了的時(shí)候甚至不太優(yōu)雅的時(shí)候廉白。其實(shí)大部分時(shí)候包一層Container組件也能做到差不多的效果,比如操作props乖寒,渲染劫持猴蹂。但其實(shí)還是有很大區(qū)別的。比如我們現(xiàn)在有兩個(gè)功能的container楣嘁,添加樣式和添加處理函數(shù)的磅轻,對(duì)Usual進(jìn)行包裝珍逸。栗子:
//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必須要引入內(nèi)層Container,進(jìn)行包裝聋溜,還有props的傳遞谆膳,同樣要注意包裝的順序。當(dāng)然你可以把所有的處理都放到一個(gè)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是更優(yōu)雅一些的把曼,每個(gè)HOC都定義自己獨(dú)有的處理邏輯杨帽,需要的時(shí)候只需要去包裝你的組件。相較于Container的方式祝迂,HOC耦合性更低睦尽,靈活性更高,可以自由組合型雳,更適合應(yīng)付復(fù)雜的業(yè)務(wù)当凡。當(dāng)然當(dāng)你的需求很簡(jiǎn)單的時(shí)候,還是用Container去自由組合纠俭,應(yīng)用場(chǎng)景需要你清楚沿量。
注意點(diǎn)(約束)
其實(shí)官網(wǎng)有很多,簡(jiǎn)單介紹一下冤荆。
最重要的原則就是朴则,注意高階組件不會(huì)修改子組件,也不拷貝子組件的行為钓简。高階組件只是通過組合的方式將子組件包裝在容器組件中乌妒,是一個(gè)無副作用的純函數(shù)
要給hoc添加class名,便于debugger外邓。我上面的好多栗子組件都沒寫class 名撤蚊,請(qǐng)不要學(xué)我,因?yàn)槲覍?shí)在想不出叫什么名了... 當(dāng)我們?cè)赾hrome里應(yīng)用React-Developer-Tools的時(shí)候损话,組件結(jié)構(gòu)可以一目了然侦啸,所以DisplayName最好還是加上。
[圖片上傳中...(image-1527ba-1521617092866-0)]靜態(tài)方法要復(fù)制
無論P(yáng)P還是II的方式丧枪,WrappedComponent的靜態(tài)方法都不會(huì)復(fù)制光涂,如果要用需要我們單獨(dú)復(fù)制。refs不會(huì)傳遞拧烦。 意思就是HOC里指定的ref忘闻,并不會(huì)傳遞到子組件,如果你要使用最好寫回調(diào)函數(shù)通過props傳下去恋博。
-
不要在render方法內(nèi)部使用高階組件服赎。簡(jiǎn)單來說react的差分算法會(huì)去比較 NowElement === OldElement, 來決定要不要替換這個(gè)elementTree葵蒂。也就是如果你每次返回的結(jié)果都不是一個(gè)引用,react以為發(fā)生了變化重虑,去更替這個(gè)組件會(huì)導(dǎo)致之前組件的狀態(tài)丟失践付。
// HOC不要放到render函數(shù)里面 class WrappedUsual extends Component { render() { const WrappenComponent = addStyle(addFunc(Usual)); console.log(this.props, 'props'); return (<div> <WrappedComponent /> </div>); } }
-
使用compose組合HOC。函數(shù)式編程的套路... 例如應(yīng)用redux中的middleware以增強(qiáng)功能缺厉。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);
關(guān)于注意點(diǎn)永高,官網(wǎng)有所介紹,不再贅述提针。鏈接
總結(jié)
高階組件最大的好處就是解耦和靈活性命爬,在react的開發(fā)中還是很有用的。
當(dāng)然這不可能是高階組件的全部用法辐脖。掌握了它的一些技巧饲宛,還有一些限制,你可以結(jié)合你的應(yīng)用場(chǎng)景嗜价,發(fā)散思維艇抠,嘗試一些不同的用法。