其實之所以講到這里是因為,當我們使用React的組件化開發(fā)Web應用的時候,就會遇到這樣的問題,很多組件需要某個功能,但是對應的功能與界面并沒有關(guān)系,無法直接簡單的抽取成為一個組件,但是如果說將類似的功能在不同的組件當中實現(xiàn)的話,就違背了所謂的Don't Your Repeat(DRY原則)
因此我們就需要使用到本節(jié)中講到的內(nèi)容 React高級組件HOC(High Order Component):
其中對應的內(nèi)容如下:
- 高級組件的概念及其應用
- 以函數(shù)為子組件的模式
最終的目的就是最大程度的代碼之間的復用,對應的兩者的策略不同,我們也應該針對特定的應用場景進行選擇!
01|什么是HOC(High Order Component)?
簡單來講HOC并不是React提供的某種特定的API,而是一種模式,增強現(xiàn)有組件的功能,對組件進行功能拓展!
- 高階組件接受一個函數(shù)作為輸入,返回一個新的組件作為結(jié)果 對應的結(jié)果對原有的輸入進行了功能上的增強!
可能我這么說的話,時機上來看還是不是特別的直觀!我們使用代碼來進行簡單的演示:
import React,{Component} from "react";
const removeUserProp = WrappedComponent=>{
return class WrappingComponent extends Component{
render(){
const {user,...otherProps} = this.props;
return (
<WrappedComponent {...anotherProp} />
);
}
}
}
其中我們使用函數(shù)表達式的方式寫了一個高階函數(shù):
- 接受一個現(xiàn)有的組件
WrappedComponent
作為輸入?yún)?shù) - 返回一個組件類
WrappingComponent
,對應的render的結(jié)果就是 剝離了user信息的被包裝的組件! - 其中所謂的增強其實就是 屏蔽了不需要的屬性字段
那么通過這個簡單的代碼,我們又能夠想到什么?
- 如果說別的組件需要使用類似的通能的話或者同樣的功能,就能夠直接使用高階組件進行
增強
了!
那么對應的根據(jù)新組件和傳入組件參數(shù)的關(guān)系,高階組件的實現(xiàn)方式可以分為兩大類;
- 代理的方式實現(xiàn)高階組件
- 繼承的方式實現(xiàn)高階組件
01|代理方式的高階組件
第一個代碼示例其實就是所謂的代理的方式的高階組件,返回的組件直接繼承React.Component類
- 新組件扮演的角色就是:傳入?yún)?shù)的代理
- 新建的render函數(shù)中把被包裹的組件渲染出來
- 高階組件做的工作通常是額外的功能增強,除此之外的工作都交給被包裹的組件
- 如果說,高階組件所做的功能除了render之外的聲明函數(shù)都不涉及的話,也不需要維護自身的狀態(tài)的話,就可以以函數(shù)組件的方式返回!
const removeUserProp = WrappedComponent=>{
return function WrappingComponent(props){
const {user,...otherProps} = props;
return <WrappedComponent {...otherProps} />
}
}
這樣依賴對應的邏輯更加的清晰,但是這種所謂的函數(shù)組件的功能是非常有限的,因此我們主要介紹class組件的方式:
代理方式的高階組件可以應用在下列場景當中:
- 操作prop
- 訪問ref
- 抽取狀態(tài)
- 包裝組件
- 操作prop
代理類型高階組件返回的組件(增強的組件),渲染的過程是由新組件的render函數(shù)所控制的!
那么也就是說,被包裹的組件如何使用是由render所決定的! 可以看成是一個代理
render函數(shù)中,新組件的this.props包含所有新組建所接收到的屬性,最簡單的方式就將是接收到的所有屬性{this.props}原封不動的傳遞給被包裹的組件 因為是做功能方面的增強,我們一般都是 增刪改props的方式傳遞給被包裹的組件!
import React,{Component} from "react";
const appendProps = (WrappedComponent,newProps)=>{
return class WrappingComponent extends Component{
render(){
return <WrappedComponent {...this.props} {...newProps}/>
};
}
}
通過以上代碼就很好的詮釋了增強功能,并且方便給不同的組件添加不同的屬性!
- 訪問ref(reference)
訪問ref并不是React所推薦的做法,但是是可以使用HOC高階組件實現(xiàn)這種功能
import React,{Component} from "react";
const refHoc = WrappedComponent=>{
return class WrappingComponoent extends Component{
constructor(){
super(...arguments);
this.linkRef = this.linkRef.bind(this);
}
linkRef(wrappedInstance){
this._root = wrappedInstance;
}
render(){
const props = {...this.props,ref:this.linkRef};
return <WrappedComponent {...props} />
}
}
}
以上代碼在linkRef被調(diào)用的時候,就得到了被包裹組件(WrappedComponent)的DOM實例,被保存在了新組件的_root屬性中!
但是實際上用的還是比較少!
- 抽取狀態(tài)
其實對應的我們在使用react的時候,react-redux中的connect函數(shù)執(zhí)行完畢返回的函數(shù)是作為高階組件的!
其中我們可以通過 傻瓜組件 和 容器組件 進行理解:
- 傻瓜組件:無狀態(tài)的組件,負責視圖的渲染
- 容器組件:負責將狀態(tài)傳遞給傻瓜組件,不負責視圖
傻瓜和容器就是妥妥的抽取狀態(tài),我們通過代碼,簡單理解一下connect高階組件
import React,{Component} from "react";
const doNothing = ()=>({});
const connect = (mapStateToProps=doNothing,mapDispatchToProps=doNothing)=>{
return function(WrappedComponent){
class HocComponent extends Component{
//lifeCycle fucntion
}
HocComponent.contextTypes = {
store:React.PropTypes.object
}
return HocComponent;
}
}
和react-redux中的connect方法一樣,我們定義的connect方法接收兩個參數(shù),分別是mapStateToProps和mapDispatchToProps.返回的組件類預期能夠訪問一個叫做store的context的值! 對應的context的值由Provider提供! 對應的我們通過this.context.store
進行訪問Provider提供的store實例,對應的實現(xiàn)類似的功能的話,HocComponent組件需要一系列的成員函數(shù)來維持內(nèi)部狀態(tài)和store同步,對應的代碼如下所示:
import React,{Component} from "react";
import {PropTypes} from "prop-types";
class HocComponent extends Component{
constructor(){
super(...arguments);
this.onChange = this.onChange.bind(this);
this.stroe = {};
}
componentDidMount(){
this.context.store.subscribe(this.onChange);
}
componentWillUnmount(){
this.context.store.unsubscribe(this.onChange);
}
onChange(){
this.setState({});
}
}
通過借助store的subscribe和unsubscribe函數(shù),HocComponent保證了每當Redux的Store上狀態(tài)發(fā)生變化的時候,都會驅(qū)動組件的更新!
雖然誰應該返回一個有狀態(tài)的組件,但是真正的組件是存在于Redux上的Store上面的,組件內(nèi)的狀態(tài)是什么其實對應的并不重要,使用組件狀態(tài)唯一原因是通過this.setState對狀態(tài)進行更新的過程!
對應的當Redux上面的Store發(fā)生了變化的時候,就可以通過this.setState重設組件狀態(tài),驅(qū)動組件更新的過程!
對應的HocComponent的render函數(shù)如下所示:
render(){
const store = this.context.store;
const newProps = {
...this.props,
...mapStateToProps(store.getState()),
...mapDispatchToProps(store.dispatch);
}
return <WrappedComponent {...newProps}/>;
}
render中的邏輯類似于"操縱Props"的方式,渲染工作完全交給了WrappedComponent,但是控制住了WrappedComponent的props,該組件能夠渲染什么完全取決于props!
以上的代碼,通過store.getState和store.dispatch可以傳遞給WrappedComponent組件對應的狀態(tài)和dispatch方法!
最終通過調(diào)用connect(mapStateToProps,mapDispatchToProps)(WrappedComponent)
里面?zhèn)魅氲慕M件進行了功能增強!
- 繼承方式的高階組件
繼承方式的高階組件采用繼承關(guān)系關(guān)聯(lián)作為參數(shù)的組件和返回的組件!如果說傳入的參數(shù)為WrappedComponent那么對應的返回的組件則直接繼承WrappedComponent
對應的我們可以使用這種方式重新實現(xiàn)一遍之前介紹的高級組件的代碼演示示例:
const removeUserProps = WrappedComponent=>{
return class NewComponent extends WrappedComponent{
render(){
const {user,...otherProps} = this.props;
this.props = otherProps;
return super.render();
}
}
}
對應的代理和繼承最大的區(qū)別就在于,使用被包裹組件的方式
- 在代理方式下:
<WrappedComponent {...otherProps} />
- 在繼承的方式下:
return super.render()
因為我們創(chuàng)造的組件繼承WrappedComponent因此直接調(diào)用父類的渲染函數(shù)即可!
代理方式下產(chǎn)生的新組件和參數(shù)組件是兩個不同的組件,一次渲染兩個組件都需要經(jīng)歷各自的生命周期!
繼承方式下兩者合而為一!只有一個生命周期!
但是以上的代碼修改this.props,對應的做法不太妥當,實際上這樣處理的話不太合適,繼承方式的高階組件可以應用與下列場景:
- 操作Porps
- 操作生命周期函數(shù)
- 操作Props
集成方式的高階組件對應的也能夠操作Props,除了上面不安全的直接修改this.props的方法,還能夠利用React.cloneElement
讓組件重新繪制!
const modifyPropsHOC = WrappedComponent=>{
return class newComponent extends WrappedComponent{
render(){
const elements = super.render();
const newStyle = {
color:(elements && elements.type === div) ? "red" : "blue"
}
const newProps = {...this.props,style:newStyle};
return React.cloneElement(elements,newProps,elements.props.children);
}
}
}
React.cloneElement(
element,
[props],
[...children]
)
以 element
元素為樣板克隆并返回新的 React 元素。返回元素的 props 是將新的 props 與原始元素的 props 淺層合并后的結(jié)果睡腿。新的子元素將取代現(xiàn)有的子元素假褪,而來自原始元素的 key
和 ref
將被保留贝或。
以上的代碼添加了一個新的屬性,樣式,如果說元素存在并且對應的頂層元素類型為div的話,那么就設置color顏色為紅色否則為藍色!
最后將本身所存在的props和心得prop屬性合并在一起! 最后通過React.cloneElement
讓產(chǎn)生新組件重新渲染一遍!
- 操作生命周期函數(shù)
因為繼承方式的高階函數(shù)返回的新組件繼承了參數(shù)組件,因此可以重新定義任何一個React組件的生命周期函數(shù),對應的這是繼承方式高階函數(shù)的特用的場景,則代理的方式無法修改傳入組件的生命周期函數(shù)
如果說參數(shù)組件只有在用戶登錄的時候才能夠渲染對應的界面的時候,那么對應的代碼則如下所示:
const OnlyForLoggedInHOC = WrappedComponent=>{
return class NewComponent extends WrappedComponent{
render(){
if(this.props.loggedIn){
return super.render();
}else{
return null;
}
}
}
}
其中還可以通過shouldComponentUpdate函數(shù),只要prop中的useCache不為邏輯false就不做重新渲染
const cacheHOC = WrappedComponent=>{
return class newComponent extends WrappedComponent{
shouldComponentUpdate(nextProps,nextState){
return !nextProps.useCache;
}
}
}
其實由此便可以看出,代理和繼承的兩種方式,各方面看來代理還是要優(yōu)于繼承方式!
優(yōu)先考慮組合,之后再考慮繼承
03|高階組件的顯示名
其實使用了所謂的高階組件都會產(chǎn)生一個新的組件,使用該組件就丟失掉了參數(shù)組件的 顯示名,因此往往需要給告誡組件重新定義一個 顯示名 不然的話,在我們debug或者說查看日志的時候組件名的顯示就會非常的莫名其妙!
如何給組件添加顯示名?
- 高階組件類的displayName添加一個字符串類型的值
如果說我們需要在react-redux中的connect返回的作為高階函數(shù),高階組件的名字包含Connect,同時包含參數(shù)組件WrappedComponent的名字,因此需要這么設置!
const getDisplayName = WrappedComponent=>{
return WrappedComponent.displayName || WrappedComponent.name || "Component";
}
HOCComponent.displayName = `Connect(${getDisplayName(WrappedComponent)})`;
04|曾經(jīng)的Mixin
除了上面所說到的高階組件,其實React中還有一種進行代碼復用的方式,就叫做Mixin,但是我們并不推薦使用它!
const shouldUpdateMixin = {
shouldComponentUpdate(){
reurn !this.props.useCache;
}
}
但是它存在對應的局限性,Mixin只能夠在用React.clearClass的語法創(chuàng)建的組建才能夠使用,如果說ES6的class語法則不適用
const SampleComponent = React.createClass({
mixins:[shouldUpdateMixin],
render(){
//render view
}
})
使用React.createClass的方式創(chuàng)建的SampleComponent的組件,因為有Mixins字段,成員方法中就混入了shouldUpdateMixin這個對象里面的方法!
那么為什么不推薦使用Mixin呢?
- 過于靈活
- 作為設計原則,我們應該盡量將state從組件中抽離出來,mixin則鼓勵在React組件中添加狀態(tài)
- ES6中的Class語法不支持Mixin,并且同時被官方廢棄了!
02|以函數(shù)作為子組件
高階組件并不是作為提高React組件代碼重用的唯一方法,高階組件通過拓展原有組件的功能的主要方法是通過對Props的控制(增加/減少/修改Props)進行的!
代理方式的高階組件,返回的組件和輸入的組件說到底是兩個組件(原有組件和功能"增強"組件)對應的父子關(guān)系,對應的組件通信方式是通過props來進行通信的!
但是對應的高階組件也有缺點,就是對原組件的Props有了固化的要求,也就是說,能不能把一個高階組件作用于某個組件X,先看一下這個組件X是不是能夠接受高階組件傳過來的props,如果說組件X并不能夠支持這些props的話,或者說對這些props的命名有所不同的話,是不能夠引用這個高階組件的!
假設有一個高階組件addUserProp,讀取對應的loggedinUser,把這個數(shù)據(jù)作為名為user的prop傳遞給參數(shù)組件
import React,{Component} from "react";
const addUserProp = WrappedComponent=>{
return class WrappingComponent extends Component{
render(){
const newProps = {user:loggedinUser};
return <WrappedComponent {...this.props} {...newProps} />;
}
}
}
像前文中所說的那樣,那么作為參數(shù)的組件,需要能夠接受名為user的prop,不然對應的高階組件完全沒效果!
但是如果說作為層層傳遞的props,這種高階組件這種要求參數(shù)組件必須和自己有契約的方式,會帶來很多麻煩!
因此為了更好地解決該問題,于是我們就使用 以函數(shù)為子組件的方式,其實就是為了客服高階組件的這種局限而生的!實現(xiàn)代碼重用的不是一個函數(shù)而是一個真正的React組件! 對應的約束其實也是這樣的:
- 子組件必須是一個函數(shù)
- 在組件實例的生命周期中,this.props.children引用的就是自組件,render函數(shù)直接會把this.props.children當作函數(shù)來調(diào)用!
接下來我們按照上面的需求重新使用函數(shù)為子組件的方式重新實現(xiàn)一遍:
import React,{Component} from "react";
import {PropTypes} from "prop-types";
const loggedinUser = 'mock user';
class AddUserProp extends Component{
render(){
const user = loggedinUser;
return this.props.children(user);
}
}
AddUserProp.propTypes = {
children:PropTypes.func.isRequired
}
以上代碼中與被增強代碼的聯(lián)系就是this.props.children
對應的children
為函數(shù)類型!在render函數(shù)中調(diào)用this.props.children
參數(shù)就是我們傳遞下去的user!
如果說想讓一個組件把user顯示出來,代碼就是這樣的:
<AddUserProp>
{user=><div>{user}</div>}
</AddUserProp>
如果說我們想將user作為prop傳遞給一個接受user名prop的組件Foo,那么只需要使用另外一個函數(shù)作為AddUserProp的子組件就行了:
<AddUserProp>
{user=><Foo user={user} />}
</AddUserProp>
03|一個倒計時的高階組件
我們利用以函數(shù)為子組件的模式構(gòu)建一個復雜一點的CountDown實現(xiàn)倒計時的通用功能:
- 初始化組件
import React,{Component} from "react";
class CountDown extends Component{
constructor(){
super(...arguments);
this.state = {count:this.props.startCount};
}
}
首先需要有一個開始的值,對應的倒計時組件的作用就是持續(xù)驅(qū)動子組件進行更新操作!
當對應的CountDown組件完成掛載之后我們就需要通過setInterval函數(shù)啟動沒秒倒計時更新內(nèi)部的狀態(tài):
componentDidMount(){
this.intervalHandle = setInterval(()=>{
const newCount = this.state.count - 1;
if(newCount>=0){
this.setState({count:newCount});
}else{
window.clearInterval(this.intervalHandle);
}
},1000);
}
使用this.intervalHandle
作為間隔調(diào)用的標記,當對應的新的count值小于且不等于0的時候就清除該標記!
其中一定要在componentDidUnmount里面一定要取消并且清理掉intervalHandle,因為CountDown完全可能在沒有倒計時為0的時候被卸載! 因此卸載之前一定需要清除interval的標記:
componentUnmount(){
if(this.intervalHandle){
window.clearInterval(this.intervalHandle);
}
}
對應的渲染部分,和之前講到的同理:
render(){
reutrn this.props.children(this.state.count);
}
我們還需要對CountDown進行約束:
import PropTypes from "prop-types";
CountDown.propTypes = {
children:PorpTypes.func.isRequired,
startCount:PropTypes.number.isRequired
}
如果說有對應的組件需要倒計時的功能,就需要恰當?shù)膶⒑瘮?shù)作為CountDown的子組件即可:
<CountDown startCount={10}>
{
count=><div>{count}</div>
}
</CountDown>
如果說當對應的倒計時為0的時候,就顯示 新年快樂! Happy New Year!
<CountDown startCount={10}>
{
count=> <div>{count > 0 ? count : "新年快樂!"}</div>
}
</CountDown>
04|性能優(yōu)化問題
函數(shù)作為子組件的方式非常靈活,但是也有其對應的缺點,也就是說,函數(shù)作為子組件的方式難以優(yōu)化!
- 外層組件的更新過程,都需要執(zhí)行一個函數(shù)獲得子組件的實際渲染效果!
- 每次渲染都需要執(zhí)行函數(shù),無法使用
shouldComponentUpdate
進行細粒度的控制,使用高階組件可以直接使用該生命周期函數(shù)避免重新渲染! - 如果說定制外層組件的shouldComponentUpdate,每個組件對這個生命周期函數(shù)的定義不同,因此也比較麻煩!
如果說要優(yōu)化的話,對應的CountDown組件也可能會成為別的組件的子組件,因此可以針對CountDown的父組件進行shouldComponentUpdate的設置:
shouldComponentUpdate(nextProps,nextState){
return nextProps.count !== this.state.count;
}
還有一個問題就是函數(shù)形式的子組件,為了代碼清晰我們使用在jsx中定義的一個箭頭函數(shù)的方式:
<CountDown startCount={10}>
count=><div>{count}</div>
</CountDown>
- 用起來非常方便,但是每次都是新的函數(shù),this.porps.children和nextProps.props.children是否有必要性去比較?
- 雖然說需要比較,但是因為children是函數(shù),因此不能夠使用匿名函數(shù)! 因此該組件內(nèi)部的函數(shù)應該是作為具名函數(shù)存在的!
<CountDown startCount={10}>
{showCount}
</CountDown>
const showCount = count=>{
return <div>{count}</div>
}
以函數(shù)為子組件,雖然存在對應的性能問題,但是確實是一個比較不錯的方式,這種模式中代碼的靈活性和性能兩塊需要作為開發(fā)者的我們進行好好地權(quán)衡!