寫在前面(說點(diǎn)廢話)
作為一個(gè)react開發(fā)者蔽午,提起高階組價(jià),我想并不陌生酬蹋,即使是沒聽過及老,我相信,你毫無察覺的用過范抓,比如redux中的connect
函數(shù)骄恶,比如antd 3.x中的createForm
方法,甚至ES6中的filter
匕垫、map
僧鲁、reduce
也是高階函數(shù)。等等....?主題不是說“高階組件”嗎象泵?怎么說了一堆函數(shù)寞秃,組件不應(yīng)該是渲染UI的嗎?當(dāng)然了偶惠,小編之所以這么叫春寿,就是為了告訴大家高階組件也叫高階函數(shù),是高階函數(shù)在react中的叫法忽孽,那么今天绑改,小編就帶你聊一聊react的高階函數(shù)馋缅,以及它有什么樣的實(shí)際意義。
什么是高階組件
正如前面提到的绢淀,ES6的filter
萤悴、map
也可以被稱為是高階函數(shù),那么它們的特點(diǎn)我想大家也知道皆的,那就是“對一個(gè)數(shù)組做遍歷后返回一個(gè)新的數(shù)組”覆履,其實(shí)在react HOC中,也是這樣的用法费薄,在官網(wǎng)也給出了比較明確的定義
高階組件是參數(shù)為組件硝全,返回值為新組件的函數(shù)。
組件是將props裝換為UI楞抡,而高階組件是將組件轉(zhuǎn)換為另一個(gè)組件伟众。
高階組件(HOC)是 React 中用于復(fù)用組件邏輯的一種高級技巧。HOC 自身不是 React API 的一部分召廷,它是一種基于 React 的組合特性而形成的設(shè)計(jì)模式凳厢。
現(xiàn)在我們對HOC的概念和它做的主要事情都搞清楚了,那么接下來竞慢,我們就來根據(jù)一個(gè)假設(shè)的需求來寫一個(gè)高階組件(同HOC)吧先紫。
要寫一個(gè)高階組件,我們先得知道高階的使用場景筹煮,畢竟有些功能是可以用普通的封裝和復(fù)用來解決的遮精,使用HOC反而讓其變得復(fù)雜起來。
- 代碼復(fù)用败潦,邏輯抽象本冲。(也就是說,我們不再需要把一些公共邏輯暴露出來來復(fù)用劫扒,完全可以抽象化檬洞,就像不可以不用理會
connect
是怎么把redux和react聯(lián)系起來也可以正常使用reudx一樣) - Props更改,我們可以通過屬性代理的方式粟关,對原來的組件Props做出相應(yīng)的更改疮胖。
- State抽象和更改(也就是說,我們可以通過HOC包裹一個(gè)組件后闷板,讓其有其他的狀態(tài),或者對其狀態(tài)做一些處理院塞。)
通過demo來演示HOC的使用場景
上面我們說了一些概念理論性的東西遮晚,如果沒有接觸過HOC或者HOC新手來說,可能還是一頭霧水拦止,那么現(xiàn)在就用一些簡單的demo來演示一些HOC的使用場景县遣。
假設(shè)需求:現(xiàn)在我們有一個(gè)數(shù)字展示組件糜颠,接受一個(gè)展示的數(shù)字,他的父組件是一個(gè)“加法器”組件萧求,每次點(diǎn)擊按鈕其兴,會將狀態(tài)+1,然后傳給展示組件展示夸政。如下圖那樣
代碼如下:
父組件:
import React, { useState } from 'react'
import CountView from './components/hoc1'
import { Button } from 'antd'
const Hoc = () => {
const [count, setCount] = useState(1)
const btnClick = () => {
setCount(count + 1)
}
return (
<>
<h1>HocDemo測試</h1>
<CountView
count = {count}
/>
<Button onClick={btnClick} type="primary" style={{ marginTop: '15px'}}>點(diǎn)我+1</Button>
</>
)
}
export default Hoc
子組件(數(shù)字展示組件)
import React from 'react'
const CountView = (props: any) => {
const { count } = props
return (
<div
style={{
width: '50px',
height: '50px',
background: '#090',
color: '#fff',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
>
{count}
</div>
)
}
export default CountView
可以看出元旬,我們的代碼非常簡單(實(shí)際上,我們面對的可能是一個(gè)非常復(fù)雜的子組件和交互邏輯)守问,現(xiàn)在我們需要將需求變更:要對當(dāng)前的展示數(shù)字乘2匀归。當(dāng)前了,如果要完成這個(gè)需求耗帕,我們有更多方法穆端,假設(shè)CountView
是一個(gè)第三方組件,我們不方便改它的源碼渲染邏輯仿便,對于父組件的props邏輯更改体啰,可能是一個(gè)首選的方案,但是我們這樣的需求有好幾個(gè)地方嗽仪,并且可能后面還會改(聽起來很變態(tài)的產(chǎn)品需求)狡赐,那么去修改父組件的邏輯就會變得工作量很大,也很麻煩钦幔,接下來枕屉,讓我們用高階組件的方式來實(shí)現(xiàn)代碼的復(fù)用吧。
定義高階組件
通過上面的概念講解鲤氢,我們了解到搀擂,高階組件是參數(shù)為組件,返回值為新組件的函數(shù)卷玉。哨颂,那么我們這樣去定義:
import React from 'react'
const double = (WrappedComponent: any) => {
return (props: any) => {
const { count } = props
return (
<WrappedComponent count={count * 2} />
)
}
}
export default double
const CountView = (props: any) => {
const { count } = props
return (
<div
style={{
width: '50px',
height: '50px',
background: '#090',
color: '#fff',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
>
{count}
</div>
)
}
export default double(CountView)
實(shí)現(xiàn)了功能:
通過上面的代碼和效果威恼,我們應(yīng)該知道
HOC 不會修改傳入的組件,也不會使用繼承來復(fù)制其行為寝并。相反箫措,HOC 通過將組件包裝在容器組件中來組成新組件。HOC 是純函數(shù)衬潦,沒有副作用斤蔓。
實(shí)際上,上面的功能中镀岛,高階組件接收被包裹組件CountView
的所有props,我們只是將props做了更改弦牡,然后傳給了CountView
組件,這樣友驮,如果我們只需要在需要做此需求更改的地方,套上這個(gè)高階組件就好了驾锰,如果有邏輯修改卸留,我們也只需要修改高階組件就好了。
細(xì)想一下椭豫,是不是和我們上面提到的高階組件的作用一樣呢耻瑟?
- 代碼復(fù)用,邏輯抽象捻悯。(也就是說匆赃,我們不再需要把一些公共邏輯暴露出來來復(fù)用,完全可以抽象化今缚,就像不可以不用理會
connect
是怎么把redux和react聯(lián)系起來也可以正常使用reudx一樣)- Props更改算柳,我們可以通過屬性代理的方式,對原來的組件Props做出相應(yīng)的更改姓言。
實(shí)際上瞬项,我們上面的高階組件的形式叫做屬性代理,是高階組件的一個(gè)主流使用方式何荚。這里有一個(gè)小彎囱淋,為什么高階組件返回組件的props會和被包裹組件的props一樣呢?畢竟是兩個(gè)組件餐塘,如果你光看高階組件的邏輯妥衣,這里很難理解,感覺很繞戒傻,如果你把眼光放在父組件税手,也就是使用“被包裹后的組件”的地方,你會發(fā)現(xiàn):我們在使用被高階組件包裹后的組件和使用不被高階組件包裹的組件需纳,他的使用方式完全一樣芦倒,所以也就導(dǎo)致了,我們傳到高階組件里面的props和被包裹組件應(yīng)有的props完全一樣不翩,這就是高階組件屬性代理的原理兵扬,同時(shí),這也體現(xiàn)了他的便利口蝠,我們在封裝完高階組件之后器钟,只需要在使用他的地方做一個(gè)調(diào)用即可,對于原邏輯并不造成任何影響亚皂。
上面在說高階組件優(yōu)點(diǎn)的時(shí)候俱箱,還有一個(gè)“State抽象和更改”,這里同樣做一下demo演示灭必,我們需要點(diǎn)擊數(shù)字展示區(qū)域的時(shí)候狞谱,改變他的背景顏色,我們對代碼做一下修改:
CountView組件
import React from 'react'
import double from './double'
const CountView = (props: any) => {
const { count, bg, divClick } = props
return (
<div
style={{
width: '50px',
height: '50px',
background: bg,
color: '#fff',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
onClick={divClick}
>
{count}
</div>
)
}
export default double(CountView)
Hoc
import React, { useState } from 'react'
const double = (WrappedComponent: any) => {
return (props: any) => {
const [ bg, setBg] = useState('#090')
const { count } = props
const divClick = () => {
setBg('#900')
}
return (
<WrappedComponent divClick={divClick} bg={bg} count={count * 2} />
)
}
}
export default double
上面的代碼中播歼,我們做了這么幾件事伶跷,首先我們要做得演示是抽象state,這里無論是類組件還是函數(shù)組件使用hooks的狀態(tài),都是可以改變的秘狞,上面的代碼中叭莫,我們做了這樣的事情。
- 將CountView組件的背景和點(diǎn)擊事件通過props抽象出來烁试,如果不抽象的話雇初,我們應(yīng)該將顏色狀態(tài)和點(diǎn)擊事件都定義在本組件中,但是我們可以使用高階組件來抽象减响,那么就是將狀態(tài)抽離到高階組件返回的組件中靖诗,然后通過高階組件屬性代理的方式,將props傳回來支示,這樣刊橘,我們就在不修改調(diào)用方式的情況下,實(shí)現(xiàn)了狀態(tài)的抽象(讓狀態(tài)不存在于本組件中)颂鸿。
- 在高階組件返回的組件中促绵,我們將狀態(tài)和事件定義在其中,再通過props傳給被包裹組件嘴纺,這樣败晴,我們將被包裹組件的
state
抽象化了。
小結(jié)
- 高階組件的實(shí)現(xiàn)方法颖医,其實(shí)包括以下兩種
- 屬性代理
- 反向繼承
我們上面的demo和講解都是講解的屬性代理位衩,關(guān)于反向繼承,后面有時(shí)間有機(jī)會再講(有點(diǎn)難)熔萧,感興趣的同學(xué)可以自行學(xué)學(xué)糖驴,實(shí)際上,因?yàn)楝F(xiàn)在類組價(jià)用的并不多佛致,反向繼承也就用的不多了贮缕。一般的,我們通過屬性代理的方式就可以解決大部分問題俺榆。
- 不要改變原始組件感昼,使用組合
我們在做高階組件的封裝的時(shí)候,不要試圖通過prototype
或者其他方式修改組件罐脊!比如下面的demo(摘自官網(wǎng))
function logProps(InputComponent) {
InputComponent.prototype.componentDidUpdate = function(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
};
// 返回原始的 input 組件定嗓,暗示它已經(jīng)被修改蜕琴。
return InputComponent;
}
// 每次調(diào)用 logProps 時(shí),增強(qiáng)組件都會有 log 輸出宵溅。
const EnhancedComponent = logProps(InputComponent);
我們在HOC中凌简,修改了被包裹組件的生命周期方法,讓他打印一些東西恃逻,這樣就會帶來下面的問題
- 被包裹組件再也無法像 HOC 增強(qiáng)之前那樣使用了雏搂,因?yàn)樗呀?jīng)被修改。
- 如果你再用另一個(gè)同樣會修改 componentDidUpdate 的 HOC 增強(qiáng)它寇损,那么前面的 HOC 就會失效凸郑!同時(shí),這個(gè) HOC 也無法應(yīng)用于沒有生命周期的函數(shù)組件矛市。
所以芙沥,修改傳入組件的 HOC 是一種糟糕的抽象方式。調(diào)用者必須知道他們是如何實(shí)現(xiàn)的尘盼,以避免與其他 HOC 發(fā)生沖突憨愉。這顯然讓原本可以提效的東西變得更糟糕了!
相反的卿捎,我們應(yīng)該使用組合的方式配紫,那么我們使用組合的方式,修改上面的組件
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
}
render() {
// 將 input 組件包裝在容器中午阵,而不對其進(jìn)行修改躺孝。Good!
return <WrappedComponent {...this.props} />;
}
}
}
這樣,我們實(shí)現(xiàn)了相同的功能底桂,但是不用修改原始組件植袍,也避免了和其他HOC沖突的情況,可以放心的使用這種抽象的邏輯抽離籽懦,并且也不用再關(guān)心于个,包裹的組件是函數(shù)組件還是類組件,高階組件是純函數(shù)暮顺、沒有副作用厅篓。
關(guān)于高階組件,官網(wǎng)還有一些約定捶码,本文就把這些零碎的知識點(diǎn)整合一下把~并做適當(dāng)?shù)慕忉尅?/p>
約定
將不相關(guān)的 props 傳遞給被包裹的組件
render() {
// 過濾掉非此 HOC 額外的 props领猾,且不要進(jìn)行透傳
const { extraProp, ...passThroughProps } = this.props;
// 將 props 注入到被包裝的組件中剂公。
// 通常為 state 的值或者實(shí)例方法错览。
const injectedProp = someStateOrInstanceMethod;
// 將 props 傳遞給被包裝組件
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
像上面的代碼一樣琳骡,我們把HOC中和自身無關(guān)的props透傳,也就是說,我們我們在使用這個(gè)被高階組件包裹過的組件后令宿,可能會收到一些無關(guān)的屬性叼耙,我們不應(yīng)該將這些無關(guān)的屬性傳給被包裹組件,這樣可以保證HOC的復(fù)用性和靈活性掀淘,否則旬蟋,一些無關(guān)的屬性可能影響他包裹的組件油昂。
最大化可組合性
- 并不是所有的 HOC 都一樣革娄。有時(shí)候它僅接受一個(gè)參數(shù),也就是被包裹的組件:
const NavbarWithRouter = withRouter(Navbar);
- HOC 通趁岬可以接收多個(gè)參數(shù)拦惋。
const CommentWithRelay = Relay.createContainer(Comment, config);
包裝顯示名稱以便輕松調(diào)試
HOC 創(chuàng)建的容器組件會與任何其他組件一樣,會顯示在 React Developer Tools 中安寺。為了方便調(diào)試厕妖,請選擇一個(gè)顯示名稱,以表明它是 HOC 的產(chǎn)物挑庶。
最常見的方式是用 HOC 包住被包裝組件的顯示名稱言秸。比如高階組件名為 withSubscription
,并且被包裝組件的顯示名稱為 CommentList
迎捺,顯示名稱應(yīng)該為 WithSubscription(CommentList)
注意事項(xiàng)
高階組件在使用的時(shí)候举畸,有一些需要注意的地方,這里大家一定要關(guān)注一下
-
要在 render 方法中使用 HOC
如果大家了解過react的更新機(jī)制凳枝,就應(yīng)該知道抄沮,react在更新的時(shí)候,是要做屬性或者節(jié)點(diǎn)的比較的
render() {
// 每次調(diào)用 render 函數(shù)都會創(chuàng)建一個(gè)新的 EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 這將導(dǎo)致子樹每次渲染都會進(jìn)行卸載岖瑰,和重新掛載的操作叛买!
return <EnhancedComponent />;
}
因?yàn)樵趓ender中使用HOC會導(dǎo)致每次render都創(chuàng)建一個(gè)新的HOC,這樣會導(dǎo)致性能的問題蹋订,同時(shí)組件的重載會導(dǎo)致組件或者起子組件的狀態(tài)丟失這樣肯定會帶來一些負(fù)面的影響率挣。
- 務(wù)必復(fù)制靜態(tài)方法
當(dāng)你將 HOC 應(yīng)用于組件時(shí),原始組件將使用容器組件進(jìn)行包裝露戒。這意味著新組件沒有原始組件的任何靜態(tài)方法椒功。也就是,我們在使用高階組件包裹一個(gè)組件后偶玫锋,因?yàn)槭切律傻慕M件蛾茉,所以里面的靜態(tài)方法會丟失,所以我們要把原始組件的靜態(tài)方法復(fù)制到高階函數(shù)返回的組件中撩鹿。
// 定義靜態(tài)函數(shù)
WrappedComponent.staticMethod = function() {/*...*/}
// 現(xiàn)在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);
// 增強(qiáng)組件沒有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
可以這樣修改
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// 必須準(zhǔn)確知道應(yīng)該拷貝哪些方法 :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
玩意靜態(tài)方法很多谦炬,有一些沒必要復(fù)制怎么辦?那么你可以在定義原始組件的時(shí)候,額外的導(dǎo)出他的靜態(tài)方法键思,這樣础爬,我們在使用的時(shí)候,按需引入就好了
// 使用這種方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...單獨(dú)導(dǎo)出該方法...
export { someFunction };
// ...并在要使用的組件中吼鳞,import 它們
import MyComponent, { someFunction } from './MyComponent.js';
Refs 不會被傳遞
因?yàn)槲覀兊母唠A組件使用的是屬性代理(即使是反向繼承)看蚜,ref都不會被當(dāng)成props傳遞和代理,這時(shí)候赔桌,你可能需要使用React.forwardRef
來做ref
轉(zhuǎn)發(fā)了供炎,比如下面這樣
import React, { forwardRef } from 'react'
import double from './double'
const CountView = (props: any, ref: any) => {
const { count, bg, divClick} = props
return (
<div
style={{
width: '50px',
height: '50px',
background: bg,
color: '#fff',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
onClick={divClick}
ref={ref}
>
{count}
</div>
)
}
export default double(forwardRef(CountView))
HOC
import React, { useState, useRef } from 'react'
const double = (WrappedComponent: any) => {
return (props: any, ref: any) => {
const [ bg, setBg] = useState('#090')
const WrappedComponentRef = useRef()
const { count } = props
const divClick = () => {
setBg('#900')
console.log(WrappedComponentRef.current)
}
return (
<WrappedComponent ref={WrappedComponentRef} divClick={divClick} bg={bg} count={count * 2} />
)
}
}
export default double
這樣我們就能通過ref轉(zhuǎn)發(fā)的形式正常的訪問被包裹組件中的Ref了。
因?yàn)镽ef不是props疾党,所以我們無法通過屬性代理的方式傳遞音诫。一定要通過ref轉(zhuǎn)發(fā),如果你對Ref轉(zhuǎn)發(fā)不屬性雪位,那么可以看我之前寫的一篇關(guān)于ref的文章
寫在后面
本文介紹了高階組件(HOC竭钝,也叫高階函數(shù))的用法,如果你弄名了雹洗,那你會發(fā)現(xiàn)香罐,他可以優(yōu)雅的處理好多邏輯的抽離,比普通的封裝更抽象时肿,還更加靈活庇茫,文中我是用一個(gè)加法器做得demo,現(xiàn)實(shí)開發(fā)中嗜侮,加法器這種簡單的demo肯定會很少港令,一般是比較復(fù)雜的邏輯,我們可以借助這種思想來幫我們完成更加復(fù)雜的封裝和抽離锈颗。那么肯定有小伙伴會問顷霹,高階組件到底有什么實(shí)際作用呢?有哪些業(yè)務(wù)場景需要用到呢击吱?(下面內(nèi)容可以略過)
我在寫文章的時(shí)候淋淀,也在思考這個(gè)問題,畢竟花時(shí)間去研究一個(gè)沒有實(shí)際意義的東西也是沒啥意思覆醇,我想到了兩種現(xiàn)實(shí)的實(shí)用場景
- 我們可以做公關(guān)的鑒權(quán)邏輯朵纷,比如通過屬性代理的方式,控制不同角色權(quán)限下一個(gè)組件的props渲染和事件執(zhí)行方式永脓,這樣就可以做到業(yè)務(wù)和權(quán)限的解耦袍辞。
- 統(tǒng)一格式化數(shù)據(jù)。在寫文章的時(shí)候常摧,中間和群友交流問題搅吁,正好他提出了一個(gè)問題威创,我覺得正是高階組件的使用場景,問題是這樣的谎懦。
在使用antd組件庫時(shí)候肚豺,treeData的類型是這樣的
array<{key, title, children, [disabled, selectable]}>
也就是說,當(dāng)我們的數(shù)據(jù)源不符合{title界拦,value}
的形式吸申,將會出現(xiàn)問題
對于上述問題,我當(dāng)時(shí)正在寫文章享甸,于是就想到了通過高階函數(shù)屬性代理去做截碴,具體方式,如果你讀懂了上面的文章枪萄,應(yīng)該不能想象隐岛,當(dāng)然了,上面的問題瓷翻,我們也可以使用一個(gè)方法包裹一些,返回這個(gè)tree
組件割坠,就相當(dāng)于做了一個(gè)簡單的組件封裝齐帚,就像下面這樣
如果你已經(jīng)很了解高階組件了,那么就很容易知道高階組件的優(yōu)勢了彼哼,首先同樣的問題除了
tree
組件還有treeSelect
組件对妄,甚至還有其他的組件,他們都面臨著這樣的問題敢朱,如果使用上面的封裝剪菱,那么重復(fù)工作將做不少,如果使用高階組件拴签,就不用管組件的類型是什么孝常,我們需要返回什么樣的組件處理什么樣的props了,我們只需要使用高階組件對全部屬性做代理蚓哩,單獨(dú)處理數(shù)據(jù)源就好了构灸。這樣一看是不是高階組件更靈活呢?
好了岸梨,說了這么多喜颁,就到這里吧。有問題歡迎評論探討哦~
demo git 源碼https://github.com/sorryljt/demo-hoc-double