高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件
从高阶组件的定义来看,高阶组件并不是一个组件,它就是一个函数,接受一个组件并且返回一个被包装过的新组件。
const NewComponent = higherOrderComponent(OldComponent)
React
的高阶组件在React
的第三方库中的应用是非常广泛的,理解好React
的高阶组件,有利于我们今后更好的学习React
第三方库的源码,可以让我们写出复用性更强的代码。
React
高阶组件的常见形式一般有两种,分别是属性代理(Props Proxy)
的形式和反向继承(Inheritance Inversion)
,简称II
。下面分别介绍两种形式的用法。
代理模式中你可以读取、添加、修改、删除将要传递给 WrappedComponent
的 props
。
假设我们有一个组件,用来从localStorage
中获取数据,我们可以这么做:
import React, { Component } from 'react';
class GetName extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
};
}
componentWillMount() {
const name = localStorage.getItem('name');
this.setState({
name
});
}
render() {
return (
<p>{ this.state.name }</p>
)
}
}
这是常规的写法,但是如果此时也有其他组件需要从localStorage
中获取数据,那么组件中从localStorage
中取值的部分代码就会重复。那么有什么比较好的方法可以复用这部分代码呢?当然有,下面用React
属性代理模式来重写上面的代码。
import React, { Component } from 'react';
function componentWrapper(WrappedComponent) {
class HOC extends Component {
constructor(props) {
super(props);
this.state = {
name: ''
}
}
componentWillMount() {
const name = localStorage.getItem('name');
this.setState({
name
});
}
render () {
const newProps = {
name: this.state.name
}
return (
<WrappedComponent {...this.props} {...newProps} />
)
}
}
return NewComponent;
}
class ShowName extends Component {
render() {
return (
<h1> { this.props.name }</h1>
)
}
}
const ShowNameWidthData = componentWrapper(ShowName);
这里我先定义一个函数componentWrapper
,函数的参数里面传入需要包装的组件WrappedComponent
,再在函数里面定义一个新的组件HOC
并返回出来,在这个新的组件里面可以添加获取localStorage
数据的逻辑,最后将获取到的数据通过属性传递的方式传给被包装的组件WrappedComponent
,这样被返回的组件中就可以通过this.props.name
的方式获取到localStorage
中存储的数据了。
这里存在一个问题,代码中从localStorage
中获取的key
值是固定的,是name
,如果我想获取其他key
的值呢?我们可以通过给高阶组件传入参数的方式来指定获取的key
值,代码可以这么写:
import React, { Component } from 'react';
function componentWrapper(WrappedComponent, key) {
class HOC extends Component {
constructor(props) {
super(props);
this.state = {
data: ''
}
}
componentWillMount() {
const data = localStorage.getItem(key);
this.setState({
data
});
}
render () {
const newProps = {
data: this.state.data
}
return (
<WrappedComponent{...this.props} {...newProps} />
)
}
}
return NewComponent;
}
class ShowName extends Component {
render() {
return (
<h1> { this.props.data }</h1>
)
}
}
const ShowNameWidthData = componentWrapper(ShowName, 'age');
可以看到,我们给高阶组件中传入了第二个参数来指定key
值,很好的解决了上述问题,这么写没问题,但是我们我们经常在React
的第三方库中看到如下用法:
HOC([param])([WrappedComponent])
这其实是函数柯里化
的写法func(a, b) => func(a)(b)
,下面我用函数柯里化的形式来改写上面的代码:
import React, { Component } from 'react';
function componentWrapper (key) {
return function(WrappedComponent) {
return class HOC extends Component {
constructor(props) {
super(props);
this.state = {
data: ''
}
}
componentWillMount() {
const data = localStorage.getItem(key);
this.setState({
data
});
}
render () {
return (
<WrappedComponentdata={this.state.data} />
)
}
}
}
}
class ShowName extends Component {
render() {
return (
<h1>{ this.props.data }</h1>
)
}
}
const ShowNameWidthData = componentWrapper('age')(ShowName);
export default ShowNameWidthData;
如果熟悉es6
箭头函数(不熟悉的可以戳这里~~)的写法来改造componentWrapper
会更加简洁:
function componentWrapper = (key) => (WrappedComponent)=> {
return class HOC extends Component {
// ...省略
}
}
我们在使用react-redux的时候一般会这么使用:
connect(mapStateToProps, mapDispatchToProps)(TodoApp)
看着是不是很熟悉?没错,这就是高阶函数的典型应用之一。connect
函数的作用连接React
组件和redux store
,通过mapStateToProps
允许我们将 store
中的数据作为 props
绑定到TodoApp
组件上,mapDispatchToProps
将 action
作为 props
绑定到组件上,也会成为 TodoApp
组件的 props
。
如果我们在浏览器中用React Developer Tools
查看React
的组件树可以看到showName
组件被HOC
包裹起来了。
但是随之带来的问题是,如果这个高阶组件被使用了多次,那么在调试的时候,将会看到一大堆HOC
,针对这个问题我们可以进行优化,能够同时显示被高阶组件包裹的组件名称。这里可以通过新增一个getDisplayName
函数以及静态属性displayName
,修改后的代码如下:
import React, { Component } from 'react';
const getDisplayName = component => component.displayName || component.name || 'Component';
function componentWrapper (key) {
return function(WrappedComponent) {
return class HOC extends Component {
// 定义一个静态方法displayName来显示原有组件的名称
static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
constructor(props) {
super(props);
this.state = {
data: ''
}
}
componentWillMount() {
const data = localStorage.getItem(key);
this.setState({
data
});
}
render () {
return (
<WrappedComponent data={this.state.data} />
)
}
}
}
}
class ShowName extends Component {
render() {
return (
<h1>{ this.props.data }</h1>
)
}
}
const ShowNameWidthData = componentWrapper('age')(ShowName);
一般如果我们想在React
的父组件中使用
的时候,可以通过this.refs.myComponent
的形式获取到组件真正实例的引用。但是如果一个组件经过高阶组件的包装就无法获得WrappedComponent
的ref
了,因为我们这时候拿到的是HOC
里面返回的component
。
下面提供一个解决方法:
要想通过 ref
获取WrappedComponent
的实例,必须在React
高阶组件的render
方法中返回WrappedComponent
。
在父组件中想要获取到WrappedComponent
的实例,先定义一个getInstance
方法,并将这个方法作为属性传入高阶组件。高阶组件中当执行render
方法的时候会先判断传入的getInstance
这个属性是否是函数,如果是就返回WrappedComponent
对象的引用。
import React, { Component } from 'react';
function HOCWrapper(WrappedComponent) {
return class HOC extends Component {
render() {
let props = {
...this.props
};
if (typeof this.props.getInstance === "function") {
props.ref = this.props.getInstance;
}
return <WrappedComponent {...props} />
}
}
}
class MyComponent extends Component {
render() {
return (
<div name="bob">this is a demo</div>
)
}
}
const Demo = HOCWrapper(MyComponent);
class ParentComponent extends React.Component {
getInstance = (ref)=>{
this.wrappedInstance = ref;
}
componentDidMount() {
console.log(this.wrappedInstance)
}
render(){
return <Demo getInstance={this.getInstance} />
}
}
反向继承顾名思义就是返回的高阶组件继承了WrappedComponent
。反向继承允许高阶组件通过 this
关键字获取 WrappedComponent
,意味着它可以获取到 state
,props
,组件生命周期(component lifecycle)
钩子,以及渲染方法(render)
。
反向继承的应用之一是"渲染劫持",所谓"渲染劫持"就是高阶组件中由于继承了WrappedComponent
,因此就控制了它的render
方法,利用这点可以做各种操作。看一个经典的例子。
import React, { Component } from 'react';
function iiHOC(config) {
return function(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
const elementTree = super.render();
const {type, style = {}} = config;
if(type === 'app-style') {
return (
<div style={{ ...style }}>
{ elementTree }
</div>
)
}
return elementTree;
}
}
}
}
class MyComponent extends Component {
render() {
return (
<div>我来啦 ~~ </div>
)
}
}
export default iiHOC({type: 'app-style', style: {color: 'red', 'font-size': '20px'}})(MyComponent);
这个例子通过传入的config
参数中的type
值来判断是否加入样式渲染。注意这个这个高阶函数的写法,用到了函数的柯里化,主要是为了便于传参。
关于React
的高阶函数的总结就到这里,React
的高阶函数其实本质上和其他语言中的装饰器的概念是一致的,只是一种设计模式的相互借鉴,掌握好React
高阶组件的用法可以帮助我们很好的提高代码的封装性,由于很多React
的第三方库中也大量使用了高阶组件的设计方法,对于我们以后阅读源码也更加游刃有余。