高階組件(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 包含上次包裹的結果。它們的關系就如下圖的三個圓圈:
實際上最終得到的組件會先去 LocalStorage 取數據,然后通過 props.data 傳給下一層組件,下一層用這個 props.data 通過 Ajax 去服務端取數據,然后再通過 props.data 把數據傳給下一層,也就是 InputWithUserName。大家可以體會一下下圖尖頭代表的組件之間的數據流向:
總結
高階組件就是一個函數,傳給它一個組件,它返回一個新的組件。新的組件使用傳入的組件作為子組件。
高階組件的作用是用于代碼復用,可以把組件之間可復用的代碼、邏輯抽離到高階組件當中。新的組件和傳入的組件通過 props 傳遞信息。
高階組件有助于提高我們代碼的靈活性,邏輯的復用性。
參考:
https://segmentfault.com/a/1190000012232867
https://www.cnblogs.com/hanmeimei/p/8806340.html