React之高阶组件

React之高阶组件

  • React之高阶组件
    • 前言
    • 基本用法
      • 属性代理模式(Props Proxy)
        • 通过ref访问组件实例
      • 反向继承(Inheritance Inversion)
      • 总结

React之高阶组件

前言

高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件

从高阶组件的定义来看,高阶组件并不是一个组件,它就是一个函数,接受一个组件并且返回一个被包装过的新组件。

const NewComponent = higherOrderComponent(OldComponent)

React的高阶组件在React的第三方库中的应用是非常广泛的,理解好React的高阶组件,有利于我们今后更好的学习React第三方库的源码,可以让我们写出复用性更强的代码。

基本用法

React高阶组件的常见形式一般有两种,分别是属性代理(Props Proxy)的形式和反向继承(Inheritance Inversion),简称II。下面分别介绍两种形式的用法。

属性代理模式(Props Proxy)

代理模式中你可以读取、添加、修改、删除将要传递给 WrappedComponentprops

假设我们有一个组件,用来从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组件上,mapDispatchToPropsaction 作为 props 绑定到组件上,也会成为 TodoApp组件的 props

如果我们在浏览器中用React Developer Tools查看React的组件树可以看到showName组件被HOC包裹起来了。
React之高阶组件_第1张图片
但是随之带来的问题是,如果这个高阶组件被使用了多次,那么在调试的时候,将会看到一大堆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之高阶组件_第2张图片

通过ref访问组件实例

一般如果我们想在React的父组件中使用的时候,可以通过this.refs.myComponent的形式获取到组件真正实例的引用。但是如果一个组件经过高阶组件的包装就无法获得WrappedComponentref了,因为我们这时候拿到的是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} />
    }
}

反向继承(Inheritance Inversion)

反向继承顾名思义就是返回的高阶组件继承了WrappedComponent。反向继承允许高阶组件通过 this关键字获取 WrappedComponent,意味着它可以获取到 stateprops,组件生命周期(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的第三方库中也大量使用了高阶组件的设计方法,对于我们以后阅读源码也更加游刃有余。

你可能感兴趣的:(react)