React高階組件以及使用場景

本文章重點講述React高階組件的使用場景,附帶講述高階函數,因為兩者很類似。對比起來更容易理解

什么是高階函數:一個函數的參數是一個函數,或者 函數的返回值是一個函數,我們稱這類函數是高階函數。

例如:

const Mozi = function(name1){
   return function (name2){
        return name1 + ' ' + name2;
    };
};

上面函數的執行過程如下

> Mozi('墨子')  
<- function(name2){
        return name1 + ' ' + name2;
   }
> Mozi('墨子')('工程')  
<- '墨子 工程'

執行函數Mozi('墨子') 返回的是一個函數,我們執行 Mozi('墨子')('工程') 相當于執行了返回的函數

高階函數實際的作用是什么?

以下面例子,我們要把數組的每一項變成mozi_n的方式

我們一般會用下面的方法進行實現

function format(item) {
    return  `mozi_${item}`;
}

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var result = [];
for (var i=0; i<arr.length; i++) {
    result.push(format(arr[i]));
}
console.log(result) // ["mozi_1", "mozi_2", "mozi_3", "mozi_4", "mozi_5", "mozi_6", "mozi_7", "mozi_8", "mozi_9"]

如果用高階函數map來實現,如下

function format(item) {
    return  `mozi_${item}`;
}
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(format);  // ["mozi_1", "mozi_2", "mozi_3", "mozi_4", "mozi_5", "mozi_6", "mozi_7", "mozi_8", "mozi_9"]

上例中map把對數組的遍歷進行了抽象封裝,我們在對數組進行操作的時候不用關心map內部是怎么實現的,只需要將我們對數組的每一項的處理函數傳進去,就會返回處理后的新數組。

這樣做的好處是簡潔,通用,我們再對數組進行其他操作的時候就不需要重復寫for循環來處理,例如

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// 把數組每一項轉換成字符串,只需要一行代碼
arr.map(String);  // ["1", "2", "3", "4", "5", "6", "7", "8", "9"]

上面兩個例子來簡單描述了一下什么是高階函數,以及簡單用法。(有空再看一下MOZI中對redux connect的封裝)

什么是React高階組件:一個組件的參數是組件,并且返回值是一個組件,我們稱這類組件為高階組件

React 中的高階組件主要有兩種形式:屬性代理反向繼承
屬性代理: 是 一個函數接受一個 WrappedComponent 組件作為參數傳入,并返回一個繼承了 React.Component 組件的類,且在該類的 render() 方法中返回被傳入的 WrappedComponent 組件
屬性代理形如:

function MoziComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}

屬性代理類高階組件可以做什么?

  • 操作 props
  • 用其他元素包裹傳入的組件 WrappedComponent
  • 通過 ref 訪問到組件實例
  • 抽離 state

一:操作 props(增加新props)

function MoziComponent(WrappedComponent) {
   return class extends WrappedComponent {
        render() {
            const newProps = {
                mozi: 'mozi'
            }
      return <WrappedComponent {...this.props,..newProps} />
        }
    };
}

二:用其他元素包裹傳入的組件 WrappedComponent

function MoziComponent(WrappedComponent) {
   return class extends WrappedComponent {
        render() {
            const newProps = {
                mozi: 'mozi'
            }
      return  <div style={{display: 'block'}}>
                    <WrappedComponent/>
               </div>
        }
    };
}

三:通過 ref 訪問到組件實例(在高階組件中獲取到組件實例可以直接操作dom,比如input,但是實用場景很少,有些很難用state去實現的復雜動畫之類的效果,用ref直接操作dom更方便一些)

import React, { Component } from 'react';

const header_hoc = WrappedComponent => {
  return class  header_hocmozi extends Component {
   
      renderHeader = () => {
          return <div style={{margin:'0 auto',textAlign:'center'}}>
              全局頭部
          </div>
      }
      getRef(ComponentRef) { // 看這里
        if(ComponentRef && ComponentRef.home) {
          console.log('wrappedComponentInstance-----',ComponentRef.home)
        }
       
      }
      render() {
        const __props ={...this.props,ref:this.getRef.bind(this) }
        return <div>
               { this.renderHeader() }
               <WrappedComponent {...__props}/>
            </div>
      }
  }

}
export default header_hoc;

四:抽離 state(因為屬性代理類的高階組件可以造作一切react有的方法或者屬性,但是不建議在高階函數里操作state,因為state屬于一個組件的私有行為,一旦在高階組件里做了一些state的更改刪除之類的,容易把原有組件覆蓋或者誤操作,出了問題也很難去定位。)

反向繼承:是 一個函數接受一個 WrappedComponent 組件作為參數傳入,并返回一個繼承了該傳入 WrappedComponent 組件的類,且在該類的 render() 方法中返回 super.render() 方法。
是因為傳入組件被動地被返回的Mozi繼承,而不是 WrappedComponent 去繼承 Mozi。通過這種方式他們之間的關系倒轉了,所以叫反向繼承。
反向繼承形如:

function MoziComponent(WrappedComponent) {
   return class Mozi extends WrappedComponent {
        render() {
            return super.render();
        }
    };
}

反向繼承因為繼承了傳入的組件,所以你可以獲取傳入組件(WrappedComponent)的state,props,render,以及整個生命周期

可以用反向繼承高階組件做什么?
  • 渲染劫持
  • State 抽象和更改
1 - 條件渲染【渲染劫持】

如下,通過 props.isLoading 這個條件來判斷渲染哪個組件

const withLoading = (WrappedComponent) => {
    return class extends WrappedComponent {
        render() {
            if(this.props.isLoading) {
                return <Loading />;
            } else {
                return super.render();
            }
        }
    };
}
2 - 修改要渲染的元素【渲染劫持】

以下demo只是展示了如何通過修改element,意味著你可以在高階組件中隨意控制返回你要渲染的元素。
比如:我們的權限控制有頁面級別的,也有細粒度的按鈕級別的,渲染劫持類高階組件就最適合細粒度的把控每個小組件的渲染權限,根據接口返回是否要展示某一個按鈕 or 某一個組件。(并且不侵入具體業務代碼,如果沒有用高階組件去抽離這部分細粒度的權限控制你就需要在業務代碼中去寫,會變得很難維護)

/*
組件修改,組件級別權限控制
*/
const edit_hoc = WrappedComponent => {
  return class extends WrappedComponent {
    render() {
      const tree = super.render();
     
      let props = {
          ...tree.props,
      };

      props.children.map(item=>{
        item && item.type == 'input' && (item.props.value = '墨子工程')
      })
      
      const newTree = React.cloneElement(tree, props, tree.props.children);
      return newTree;
    }
  };
};  

export default edit_hoc;

實用場景

1- 組件渲染性能追蹤
//performance_hoc.js
const hoc = WrappedComponent => {
  return class performance_hoc extends WrappedComponent {
    constructor(props) {
        super(props);
        this.start = 0;
        this.end = 0;
    }
   componentWillMount() {
        super.componentWillMount && super.componentWillMount();
        this.start = Date.now();
    }
    componentDidMount() {
      super.componentDidMount && super.componentDidMount();
      this.end = Date.now();
      console.log(`%c ${WrappedComponent.name} 組件渲染時間為 ${this.end - this.start} ms`,'font-size:20px;color:red;');
    }
    render() {
      return super.render()
    }
  };
};  

export default hoc;

使用方式

import React, { Component } from 'react';
import hoc from './performance_hoc';
@hoc
export default class Performance extends Component {
  render() {
    return <div>墨子工程</div>
  }
}

2 - 權限控制
import React, { Component } from 'react';

const auth_hoc = role => WrappedComponent => {
  return class extends React.Component {
      state = {
          permission: false,
          loading: true
      }

      showLoading = (isShow) => {
        this.setState({
            loading: isShow,
        });
      }

      checkAuth = () => { // 模擬返回角色的api
       return new Promise((reslove,reject)=>{
          this.showLoading(true)
          setTimeout(_=>{
            reslove('admin')
            this.showLoading(false)
          },2000)
        })
      }

      async componentWillMount() {
          const __role = await this.checkAuth();
          this.setState({
              permission: __role == role,
          });
      }

      render() {
        const { loading, permission } = this.state

          if(loading) {
            return <div>loading...</div>
          }

          if (permission) {
              return <WrappedComponent {...this.props} />;
          } else {
              return (<div>您沒有權限查看該頁面!</div>);
          }
      }
  };
}
export default auth_hoc;

使用(比如:產品需求是這個頁面只有admin方式的才能展示 or 只有vip方式的才能展示,就可以通過高階組件去抽離,你也不需要每一個權限都寫一個高階組件,只需要 傳遞不同的權限去接口驗證就ok)

import React, { Component } from 'react';
import auth_hoc from './auth_hoc'; 
@auth_hoc('admin') //vip
export default class AuthPage extends Component {
  constructor(...args) {
    super(...args);
  }
  render() {
    return (
      <div className="Home">
         墨子工程
      </div>
    );
  }
}

3 - 組件&邏輯復用
//header_hoc.js
import React, { Component } from 'react';

const header_hoc = WrappedComponent => {
  return class  header_hocmozi extends Component {
   
      renderHeader = () => {
          return <div style={{margin:'0 auto',textAlign:'center'}}>
              全局頭部
          </div>
      }
      render() {
        return <div>
               { this.renderHeader() }
               <WrappedComponent {...this.props}/>
            </div>
      }
  }

}
export default header_hoc;

使用

import React, { Component } from 'react';
import header_hoc from './header_hoc';

@header_hoc
export default class Home extends Component {
  render() {
    return (
      <div className="Home">
        首頁
      </div>
    );
  }
}

高階組件其實就是裝飾器模式在 React 中的實現:通過給函數傳入一個組件,在函數內部對該組件進行功能的增強,最后返回這個組件

總結:

  • 高階組件 不是組件,是 一個把某個組件轉換成另一個組件的 函數
  • 高階組件的主要作用是 代碼復用
  • 高階組件是 裝飾器 在 React 中的實現
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372