組件復用之高階組件(Higher Order Component)

高階組件(HOC)是一個函數返回一個React組件,指的就是一個React組包裹著另一個React組件。可以理解為一個生產React組件的工廠。

什么是高階組件?

高階組件就是一個函數,傳給它一個組件,它返回一個新的組件。
高階組件是一個函數(而不是組件),它接受一個組件作為參數,返回一個新的組件。這個新的組件會使用你傳給它的組件作為子組件。

常用高階函數

  • Props Proxy(pp) HOC對被包裹組件WrappedComponent的props進行操作。
  • Inherbitance Inversion(ii)HOC繼承被包裹組件WrappedComponent。
    注意,第二種方式可能導致子組件不完全解析。

Props Proxy方式

一種最簡單的Props Proxy實現

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

這里的HOC是一個方法,接受一個WrappedComponent作為方法的參數,返回一個PP class,renderWrappedComponent。
使用的時候:

const ListHOCInstance = ppHOC(List);
<ListHOCInstance name='instance' type='hoc' />

這里是將應該傳給List組件的屬性name, type等,都傳給了它的返回值ListHOCInstance,這樣的就相當于在List外面加了一層代理,這個代理用于處理即將傳給WrappedComponent的props,這也是這種HOC為什么叫Props Proxy

在pp組件中,我們可以對WrappedComponent進行以下操作:

  • 操作props(增刪改)
  • 通過refs訪問到組件實例
  • 提取state
  • 用其他元素包裹WrappedComponent

增加props

添加新的props給WrappedComponent:

const isLogin = false;
function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      const newProps = {
        isNew: true,
        login: isLogin
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

WrappedComponent組件新增了兩個props:isNew和login

通過refs訪問到組件實例

function refsHOC(WrappedComponent) {
  return class RefsHOC extends React.Component {
    proc(wrappedComponentInstance) {
      wrappedComponentInstance.method()
    }
    render() {
      const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
      return <WrappedComponent {...props}/>
    }
  }
}

Ref 的回調函數會在 WrappedComponent 渲染時執行,你就可以得到WrappedComponent的引用。這可以用來讀取/添加實例的 props ,調用實例的方法。

不過這里有個問題,如果WrappedComponent是個無狀態組件,則在proc中的wrappedComponentInstance是null,因為無狀態組件沒有this,不支持ref, 這就需要把state提取出來,作為組件實例的內部屬性,即有狀態屬性。

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        name: ''
      }

      this.onNameChange = this.onNameChange.bind(this)
    }
    onNameChange(event) {
      this.setState({
        name: event.target.value
      })
    }
    render() {
      const newProps = {
        name: {
          value: this.state.name,
          onChange: this.onNameChange
        }
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

使用的時候:

class Test extends React.Component {
    render () {
        return (
            <input name="name" {...this.props.name}/>
        );
    }
}
export default ppHOC(Test);

高階組件應用--localStorage

import React, { Component } from 'react'

export default (WrappedComponent, name) => {
  class NewComponent extends Component {
    constructor () {
      super()
      this.state = { data: null }
    }

    componentWillMount () {
      let data = localStorage.getItem(name)
      this.setState({ data })
    }

    render () {
      return <WrappedComponent data={this.state.data} />
    }
  }
  return NewComponent
}

現在 NewComponent 會根據第二個參數 name 在掛載階段從 LocalStorage 加載數據,并且 setState 到自己的 state.data 中,而渲染的時候將 state.data 通過 props.data 傳給 WrappedComponent。

這個高階組件有什么用呢?假設上面的代碼是在 src/wrapWithLoadData.js 文件中的,我們可以在別的地方這么用它:

import wrapWithLoadData from './wrapWithLoadData'

class InputWithUserName extends Component {
  render () {
    return <input value={this.props.data} />
  }
}

InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
export default InputWithUserName

假如 InputWithUserName 的功能需求是掛載的時候從 LocalStorage 里面加載 username 字段作為 <input /> 的 value 值,現在有了 wrapWithLoadData,我們可以很容易地做到這件事情。

只需要定義一個非常簡單的 InputWithUserName,它會把 props.data 作為 <input /> 的 value 值。然把這個組件和 'username' 傳給 wrapWithLoadData,wrapWithLoadData 會返回一個新的組件,我們用這個新的組件覆蓋原來的 InputWithUserName,然后再導出去模塊。

別人用這個組件的時候實際是用了被加工過的組件:

import InputWithUserName from './InputWithUserName'

class Index extends Component {
  render () {
    return (
      <div>
        用戶名:<InputWithUserName />
      </div>
    )
  }
}

根據 wrapWithLoadData 的代碼我們可以知道,這個新的組件掛載的時候會先去 LocalStorage 加載數據,渲染的時候再通過 props.data 傳給真正的 InputWithUserName。

如果現在我們需要另外一個文本輸入框組件,它也需要 LocalStorage 加載 'content' 字段的數據。我們只需要定義一個新的 TextareaWithContent:

import wrapWithLoadData from './wrapWithLoadData'

class TextareaWithContent extends Component {
  render () {
    return <textarea value={this.props.data} />
  }
}

TextareaWithContent = wrapWithLoadData(TextareaWithContent, 'content')
export default TextareaWithContent

寫起來非常輕松,我們根本不需要重復寫從 LocalStorage 加載數據字段的邏輯,直接用 wrapWithLoadData 包裝一下就可以了。
我們來回顧一下到底發生了什么事情,對于 InputWithUserName 和 TextareaWithContent 這兩個組件來說,它們的需求有著這么一個相同的邏輯:“掛載階段從 LocalStorage 中加載特定字段數據”。

如果按照之前的做法,我們需要給它們兩個都加上 componentWillMount 生命周期,然后在里面調用 LocalStorage。要是有第三個組件也有這樣的加載邏輯,我又得寫一遍這樣的邏輯。但有了 wrapWithLoadData 高階組件,我們把這樣的邏輯用一個組件包裹了起來,并且通過給高階組件傳入 name 來達到不同字段的數據加載。充分復用了邏輯代碼。

高階組件的靈活性

代碼復用的方法、形式有很多種,你可以用類繼承來做到代碼復用,也可以分離模塊的方式。但是高階組件這種方式很有意思,也很靈活。學過設計模式的同學其實應該能反應過來,它其實就是設計模式里面的裝飾者模式。它通過組合的方式達到很高的靈活程度。

假設現在我們需求變化了,現在要的是通過 Ajax 加載數據而不是從 LocalStorage 加載數據。我們只需要新建一個 wrapWithAjaxData 高階組件:

import React, { Component } from 'react'

export default (WrappedComponent, name) => {
  class NewComponent extends Component {
    constructor () {
      super()
      this.state = { data: null }
    }

    componentWillMount () {
      ajax.get('/data/' + name, (data) => {
        this.setState({ data })
      })
    }

    render () {
      return <WrappedComponent data={this.state.data} />
    }
  }
  return NewComponent
}

其實就是改了一下 wrapWithLoadData 的 componentWillMount 中的邏輯,改成了從服務器加載數據。現在只需要把 InputWithUserName 稍微改一下:

import wrapWithAjaxData from './wrapWithAjaxData'

class InputWithUserName extends Component {
  render () {
    return <input value={this.props.data} />
  }
}

InputWithUserName = wrapWithAjaxData(InputWithUserName, 'username')
export default InputWithUserName

只要改一下包裝的高階組件就可以達到需要的效果。而且我們并沒有改動 InputWithUserName 組件內部的任何邏輯,也沒有改動 Index 的任何邏輯,只是改動了中間的高階組件函數。

多層高階組件

假如現在需求有變化了:我們需要先從 LocalStorage 中加載數據,再用這個數據去服務器取數據。我們改一下(或者新建一個)wrapWithAjaxData 高階組件,修改其中的 componentWillMount:

componentWillMount () {
      ajax.get('/data/' + this.props.data, (data) => {
        this.setState({ data })
      })
    }

它會用傳進來的 props.data 去服務器取數據。這時候修改 InputWithUserName:

import wrapWithLoadData from './wrapWithLoadData'
import wrapWithAjaxData from './wrapWithAjaxData'

class InputWithUserName extends Component {
  render () {
    return <input value={this.props.data} />
  }
}

InputWithUserName = wrapWithAjaxData(InputWithUserName)
InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
export default InputWithUserName

可以看到,我們給 InputWithUserName 應用了兩種高階組件:先用 wrapWithAjaxData 包裹 InputWithUserName,再用 wrapWithLoadData 包含上次包裹的結果。它們的關系就如下圖的三個圓圈:


image.png

實際上最終得到的組件會先去 LocalStorage 取數據,然后通過 props.data 傳給下一層組件,下一層用這個 props.data 通過 Ajax 去服務端取數據,然后再通過 props.data 把數據傳給下一層,也就是 InputWithUserName。大家可以體會一下下圖尖頭代表的組件之間的數據流向:


image2.png

總結

高階組件就是一個函數,傳給它一個組件,它返回一個新的組件。新的組件使用傳入的組件作為子組件。

高階組件的作用是用于代碼復用,可以把組件之間可復用的代碼、邏輯抽離到高階組件當中。新的組件和傳入的組件通過 props 傳遞信息。

高階組件有助于提高我們代碼的靈活性,邏輯的復用性。

參考:
https://segmentfault.com/a/1190000012232867
https://www.cnblogs.com/hanmeimei/p/8806340.html

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

推薦閱讀更多精彩內容

  • 探索Vue高階組件 高階組件(HOC)是 React 生態系統的常用詞匯,React 中代碼復用的主要方式就是使用...
    君惜丶閱讀 998評論 0 2
  • 探索Vue高階組件高階組件(HOC)是 React 生態系統的常用詞匯,React 中代碼復用的主要方式就是使用高...
    videring閱讀 10,635評論 5 30
  • 本文章將從四個方面講解高階組件 什么是高階組件? 高階組件是為了解決什么問題? 常見用法 與父組件的區別 什么是高...
    rangel閱讀 3,312評論 0 3
  • 深入JSX date:20170412筆記原文其實JSX是React.createElement(componen...
    gaoer1938閱讀 8,097評論 2 35
  • 1】從本篇文章中學到的概念: 人性的善良是存在的,我們要善于發現,嘗試著去感受,盡量幫助他人,更是相信他人的表現。...
    a6dfdbc06a45閱讀 309評論 1 0