高階組件HOC - 小試牛刀

原文地址:https://github.com/SmallStoneSK/Blog/issues/6javascript

1. 前言

老畢曾經有過一句名言,叫做「國慶七天樂,Coding最快樂~」。因此在這漫漫七天長假,手癢了怎麼辦?因而乎,就有了接下來的內容。。。css

2. 一箇中心

今天要分享的內容有關高階組件的使用。java

雖然這類文章早已經爛大街了,並且想必各位看官也是稔熟於心。所以,本文不會着重介紹一堆HOC的概念,而是經過兩個實實在在的實際例子來講明HOC的用法和強大之處。git

3. 兩個例子

3.1 例子1:呼吸動畫

首先,咱們來看第一個例子。喏,就是這個。github

呼吸動畫

是滴,這個就是呼吸動畫(錄的動畫有點渣,請別在乎。。。),想必你們在絕大多數的APP中都見過這種動畫,只不過我這畫的很是簡陋。在數據ready以前,這種一閃一閃的呼吸動畫能夠有效地緩解用戶的等待心理。web

這時,有人就要跳出來講了:「這還不簡單,建立個控制opacity的animation,再添加class不就行了。。。」是的,在web的世界中,css animation有時真的能夠隨心所欲。可是我想說,在RN的世界裏,只有Animated才真的好使。app

不過話說回來,要用Animated來作這個呼吸動畫,的確也很簡單。代碼以下:dom

class BreathLoading extends React.PureComponent {

  componentWillMount() {
    this._initAnimation();
    this._playAnimation();
  }

  componentWillUnmount() {
    this._stopAnimation();
  }

  _initAnimation() {
    this.oritention = true;
    this.isAnimating = true;
    this.opacity = new Animated.Value(1);
  }

  _playAnimation() {
    Animated.timing(this.opacity, {
      isInteraction: false,
      duration: params.duration,
      toValue: this.oritention ? 0.2 : 1,
      easing: this.oritention ? Easing.in : Easing.easeOut
    }).start(() => {
      this.oritention = !this.oritention;
      this.isAnimating && this._playAnimation();
    });
  }

  _stopAnimation = () => this.isAnimating = false;

  render = () => <Animated.View style={{opacity: this.opacity, width: 100, height: 50, backgroundColor: '#EFEFEF'}}/>;

}

是的,僅二十幾行代碼咱們就完成了一個簡單地呼吸動畫。可是問題來了,假如在你的業務需求中有5個、10個場景都須要用到這種呼吸動畫怎麼辦?總不能複製5次、10次,而後修改它們的render方法吧?這也太蠢了。。。函數

有人會想到:「那就封裝一個組件唄。反正呼吸動畫的邏輯都是不變的,惟一在變的是渲染部分。能夠經過props接收一個renderContent方法,將渲染的實際控制權交給調用方。」那就來看看代碼吧:fetch

class BreathLoading extends React.PureComponent {
  // ...省略
  render() {
    const {renderContent = () => {}} = this.props;
    return renderContent(this.opacity);
  }
}

相比較於一開始的例子,如今這個BreathLoading組件能夠被複用,調用方只要關注本身渲染部分的內容就能夠了。可是說實話,我的在這個組件使用方式上總感受有點不舒服,有一個不痛不癢的小問題。習慣上來講,在真正使用BreathLoading的時候,咱們一般會寫出左下圖中的這種代碼。因爲renderContent接收的是一個匿名函數,所以當組件A render的時候,雖然BreathLoading是一個純組件,可是先後兩次接收的renderContent是兩個不一樣的函數,仍是會發起一次沒必要要的domDiff。那還不簡單,只要把renderContent中的內容單獨抽成一個函數再傳進去不就行了(見右下圖)。

對溜,這個就是我剛纔說的不爽的地方。好端端的一個Loading組件,封裝你也封裝了,憑啥我還要分兩步才能使用。其實BB了那麼久,你也知道埋了那麼多的鋪墊,是時候HOC出場了。。。說來慚愧,在接觸HOC以前鄙人一直用的就是上面這種方法來封裝。。。直到用上了HOC以後,才發現真香真香。。。

在這裏,咱們要用到的是高階組件的代理模式。你們都知道,高階組件是一個接收參數、返回組件的函數而已。對於這個呼吸動畫的例子而言,咱們來分析一下:

  1. 接收什麼?固然是接收剛纔renderContent返回的那個組件啦。
  2. 返回什麼?固然是返回咱們的BreathLoading組件啦。

OK,看完上面的兩句廢話以後,再來看下面的代碼。

export const WithLoading = (params = {duration: 600}) => WrappedComponent => class extends React.PureComponent {

  componentWillMount() {
    this._initAnimation();
    this._playAnimation();
  }

  componentWillUnmount() {
    this._stopAnimation();
  }

  _initAnimation() {
    this.oritention = true;
    this.isAnimating = true;
    this.opacity = new Animated.Value(1);
  }

  _playAnimation() {
    Animated.timing(this.opacity, {
      isInteraction: false,
      duration: params.duration,
      toValue: this.oritention ? 0.2 : 1,
      easing: this.oritention ? Easing.in : Easing.easeOut
    }).start(() => {
      this.oritention = !this.oritention;
      this.isAnimating && this._playAnimation();
    });
  }

  _stopAnimation = () => this.isAnimating = false;

  render = () => <WrappedComponent opacity={this.opacity} {...this.props}/>;
};

看完上面的代碼以後,再回頭瞅瞅前面的那兩句話,是否是豁然開朗。仔細觀察WrappedComponent,咱們發現opacity居然以props的形式傳給了它。只要WrappedComponent拿到了關鍵的opacity,那豈不是想幹什麼就幹什麼來着,並且尚未前面說的什麼匿名函數和domDiff消耗問題。再配上decorator裝飾器,豈不是美滋滋?代碼以下:

@WithLoading()
class Test extends React.PureComponent {
  render() {
    const {opacity} = this.props;
    return (
      <View style={{marginTop: 40, paddingHorizontal: 20}}>
        <View style={{marginTop: 20, flexDirection: 'row', justifyContent: 'space-between'}}>
          <Animated.View style={{opacity, backgroundColor: '#EFEFEF', width: 150, height: 20}}/>
          <Animated.View style={{opacity, backgroundColor: '#EFEFEF', width: 150, height: 20}}/>
        </View>
        <View style={{marginTop: 20, flexDirection: 'row', justifyContent: 'space-between'}}>
          <Animated.View style={{opacity, backgroundColor: '#EFEFEF', width: 150, height: 20}}/>
          <Animated.View style={{opacity, backgroundColor: '#EFEFEF', width: 150, height: 20}}/>
        </View>
      </View>
    )
  }
}

相比之下,顯然高階組件的用法更勝一籌。之後無論要作成什麼樣的呼吸動畫,只要加一個@withLoading就搞定了。由於這個高階函數,賦予了普通組件一種呼吸閃爍的能力(記住這句話,圈起來重點考)。

3.2 例子2:多版本控制的組件

通過上面的例子,咱們初步感覺到了高階組件的黑魔法。由於經過它,咱們能讓一個組件擁有某種能力,可以化腐朽爲神奇。。。哦,吹過頭了。。。那咱們來看第二個例子,也是業務需求中會遇到的場景。爲啥?由於善變的產品常常要改版,要作AB!!!

所謂多版本控制的組件,其實就是一個擁有相同功能的組件,因爲產品的需求,經歷了A版 -> B版 -> C版 -> D版。。。這無窮無盡的改版,有的換個皮膚,改個樣式,有的甚至改了交互。

或許對於一個簡單的小組件而言,每次改版只要從新建立一個新的組件就能夠了。可是,若是對於一個頁面級別的Page組件呢?就像下面的這個組件同樣,做爲容器組件,這個組件充斥着大量複雜的處理邏輯(這裏寫的是超級簡化版的。。。實際應用場景中會複雜的多)。

class X extends Page {

  state = {
    list: []
  };

  componentDidMount() {
    this._fetchData();
  }

  _fetchData = () => setTimeout(() => this.setState({list: [1,2,3]}), 2000);

  onClickHeader = () => console.log('click header');
  
  onClickBody = () => console.log('click body');
  
  onClickFooter = () => console.log('click footer');

  _renderHeader = () => <Header onClick={this.onClickHeader}/>;

  _renderBody = () => <Body data={this.state.list} onClick={this.onClickBody}/>;

  _renderFooter = () => <Footer onClick={this.onClickFooter}/>;

  render = () => (
    <View>
      {this._renderHeader()}
      {this._renderBody()}
      {this._renderFooter()}
    </View>
  );
}

在這種狀況下,假如產品要對這個頁面作AB該怎麼辦呢?爲了方便作AB,咱們固然但願建立一個新的Page組件,而後在源頭上根據AB實驗分別跳轉到PageA和PageB便可。可是若是真的copy一份PageA做爲PageB,再修改其render方法的話,那請你好好保重。。。要否則怎麼辦嘞?另外一種很容易想到的辦法是在原來Page的render方法中作AB,以下代碼:

class X extends Page {

  // ...省略

  _renderHeaderA = () => <HeaderA onClick={this.onClickHeader}/>;

  _renderBodyA = () => <BodyA data={this.state.list} onClick={this.onClickBody}/>;

  _renderFooterA = () => <FooterA onClick={this.onClickFooter}/>;

  _renderHeaderB = () => <HeaderB onClick={this.onClickHeader}/>;

  _renderBodyB = () => <BodyB data={this.state.list} onClick={this.onClickBody}/>;

  _renderFooterB = () => <FooterB onClick={this.onClickFooter}/>;

  render = () => {
    const {version} = this.props;
    return version === 1 ? (
      <View>
        {this._renderHeaderA()}
        {this._renderBodyA()}
        {this._renderFooterA()}
      </View>
    ) : (
      <View>
        {this._renderHeaderB()}
        {this._renderBodyB()}
        {this._renderFooterB()}
      </View>
    );
  }
}

但是這種處理方式有一個很大的弊端!做爲Page組件,每每代碼量都會比較大,要是再寫一堆的renderXXX方法那這個文件勢必更加臃腫了。。。要是再改版C、D怎麼辦?並且很是容易寫出諸如version === 1 ? this._renderA() : this._renderB()之類的代碼,甚至還有各版本耦合在一塊兒的代碼,到了後期就更加無法維護了。

那你到底想怎樣。。。爲了解決上面臃腫的問題,或許咱們能夠嘗試把這些render方法給移到另外的文件中(這裏須要注意兩點:因爲this問題,咱們須要將Page的實例做爲ctx傳遞下去;爲了保證組件可以正常render,須要把state展開傳遞下去),看下代碼:

說實話,這段代碼寫的足夠噁心。。。好好的一個組件被拆得支離破碎,用到this的地方所有被替換成了ctx,還將整個state展開傳遞下去,看着就很隔應,並且很不習慣,對於新接手的人來講也容易形成誤解。因此這種hack的方式仍是不行,那麼到底應該怎麼辦呢?

噔噔噔噔,高階組件又要出場了~ 在改造這個Page以前,咱們先來想下,如今這個例子和剛纔的呼吸動畫那個例子有沒有什麼類似的地方?答案就是:許多邏輯部分都相同,不一樣點在於渲染部分。因此,咱們的重點在於控制render部分,同時還要解決this的指向問題。來看下代碼:

重點在兩處:一處是constructor的最後一句,咱們將renderEntity中方法都綁定到了Page的實例上;另外一處則是render方法,咱們經過call的方式巧妙地修改了this的指向問題。這樣一來,對於PageA和PageB而言,就徹底用不到ctx了。咱們再來對比下原來的Page組件,利用高階組件,咱們徹底就是將相關的render方法挪了一個位置而已,無形之中還保證了本次修改不會影響到原來的功能。

到了這兒,問題彷佛都迎刃而解,但其實還有一個瑕疵。。。啥?到底有完沒完。。。不信,這時候你給PageB中的子組件再加一個onPressXXX事件試試。是哦,這時候事件該加在哪兒呢。。。很簡單,有了renderEntity這個先例,再來一個eventEntity不就行了嗎。。。看下代碼:

真的是不加不知道,一加嚇一跳。。。有了eventEntity以後,思路瞬間豁然開朗。由於經過eventEntity,咱們能夠將PageA,PageB的事件各自管理,邏輯也被解耦了。咱們能夠將各版本Page通用的事件仍然保留在Page中,可是各頁面獨有的事件寫在各自的eventEntity中維護。要是往後再想添加新版本的PageC、PageD,或是廢棄PageA,維護管理起來都很是方便。

按照劇情,逼也裝夠了,其實到這裏應該要結束了,但是誰讓我又知道了高階組件的反向繼承模式呢。。。前一種的方法惟一的缺點就在於爲了hack,咱們無形中將PageA和PageB拆的支離破碎,各類方法散落在Object的各個角落。而反向繼承的巧妙之處就在於高階函數返回的能夠是一個繼承自傳進來的組件的組件,所以對於以前的代碼,咱們只要稍加改動便可。看下代碼:

相比前一種方法,如今的PageA、PageB顯得更加組件了。因此啊,這繞來繞去的,到頭來卻感受就只邁出了一小步。。。還記得剛纔說要圈起來重點考的那句話嗎?對於這個多版本組件的例子,咱們只不過是利用高階組件的形式賦予了PageA,B,C,D這類組件處理該頁面業務邏輯的能力。

4. 三點思考

4.1 高階組件有啥好處?

想必經過上面的兩個實際例子,各位看官多多少少已經夠體會到高階組件的好處,由於它確實可以幫助解決平時業務開發中的痛點。其實,高階組件就是把一些通用的處理邏輯封裝在一個高階函數中,而後返回一個擁有這些邏輯的組件給你。這樣一來,你就賦予了一個普通組件某種能力,同時對該組件的入侵也較小。因此啊,若是你的代碼中充斥着大量重複性的工做,還不趕忙用起來?

4.2 啥時候用高階組件?

雖然是建議用高階組件來解決問題,但可千萬別啥都往高階組件上套。。。實話實說,我還真見過這樣的代碼。。。可是其實呢,高階組件自己也只是封裝組件的一種方式而已。就比方說文中Loading組件的那個例子,不用高階不照樣能封裝一個組件來簡化重複性工做嗎?

那究竟何時用高階比較合適呢?還記得先前強調了兩遍的那句話麼?「高階組件能夠賦予一類組件某種能力」 注意這裏的關鍵詞【一類】,在你準備使用高階組件以前想想,你接下來要作的事情是否是賦予一類組件某種能力?不妨回想一下上面的兩個例子,第一個例子是賦予了一類普通組件可以呼吸動畫的能力,第二個例子是賦予一類Page組件可以處理當前頁面業務邏輯的能力。除此以外,還有一個例子也是特別合適,那就是Animated.createAnimatedComponent,它也是賦予了一類普通組件可以響應Animated.Value變化的能力。因此啊,某種程度上你能夠把高階組件理解爲是一種黑魔法,一旦加上了它,你的組件就能擁有某種能力。這個時候,使用高階組件來封裝你的代碼再合適不過了。

另外,高階組件還有一項很是厲害的優點,那就是能夠組合。固然了,本文的例子並無體現出這種能力。可是試想,假如你手上有許多個黑魔法(即高階組件),當你把它們自由組合在一塊兒加到某個組件上時,是否是能夠創造出無限的可能?而相反,若是你在封裝一個組件的時候集成了所有這些功能,這個組件勢必會很是臃腫,而當另外的組件須要其中某幾個相似的功能時,代碼還不能複用。。。

4.3 該怎麼使用高階組件?

高階組件其實共分爲兩種模式:屬性代理 和 反向繼承。分別對應上文中的第一個、第二個例子。那該怎麼區分使用呢?嘿嘿,本身用用就知道了。看的再多,不如本身動手寫一個來的理解更深。本文不是高階組件的使用教程,只是兩個用高階組件解決實際問題的例子而已。要真想進一步深刻了解高階組件,能夠看介紹高階組件的文章,而後動手實踐慢慢體會~ 等到你回過頭來再想一下的時候,一定會有一種豁然開朗的感受。

5. 寫在最後

都說高階組件大法好,之前都嗤之以鼻,直到抱着試一試的心態才發現。。。

真香真香。。。

相關文章
相關標籤/搜索