高阶组件(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
}
}
}
这里的HOC是一个方法,接受一个WrappedComponent作为方法的参数,返回一个PP class,renderWrappedComponent。
使用的时候:
const ListHOCInstance = ppHOC(List);
这里是将应该传给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组件新增了两个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
}
}
}
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
}
}
}
使用的时候:
class Test extends React.Component {
render () {
return (
);
}
}
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
}
}
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
}
}
InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
export default InputWithUserName
假如 InputWithUserName 的功能需求是挂载的时候从 LocalStorage 里面加载 username 字段作为 的 value 值,现在有了 wrapWithLoadData,我们可以很容易地做到这件事情。
只需要定义一个非常简单的 InputWithUserName,它会把 props.data 作为 的 value 值。然把这个组件和 'username' 传给 wrapWithLoadData,wrapWithLoadData 会返回一个新的组件,我们用这个新的组件覆盖原来的 InputWithUserName,然后再导出去模块。
别人用这个组件的时候实际是用了被加工过的组件:
import InputWithUserName from './InputWithUserName'
class Index extends Component {
render () {
return (
用户名:
)
}
}
根据 wrapWithLoadData 的代码我们可以知道,这个新的组件挂载的时候会先去 LocalStorage 加载数据,渲染的时候再通过 props.data 传给真正的 InputWithUserName。
如果现在我们需要另外一个文本输入框组件,它也需要 LocalStorage 加载 'content' 字段的数据。我们只需要定义一个新的 TextareaWithContent:
import wrapWithLoadData from './wrapWithLoadData'
class TextareaWithContent extends Component {
render () {
return
}
}
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
}
}
return NewComponent
}
其实就是改了一下 wrapWithLoadData 的 componentWillMount 中的逻辑,改成了从服务器加载数据。现在只需要把 InputWithUserName 稍微改一下:
import wrapWithAjaxData from './wrapWithAjaxData'
class InputWithUserName extends Component {
render () {
return
}
}
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
}
}
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