React

React 高阶组件

2017-06-02  本文已影响111人  柏丘君

高阶组件是对既有组件进行包装,以增强既有组件的功能。其核心实现是一个无状态组件(函数),接收另一个组件作为参数,然后返回一个新的功能增强的组件。
看一个最简单的例子:
App.js:

import React,{ PureComponent } from "react";

const DisplayUser = (props) => {
    return(
        <div>
            姓名:{ props.username }
        </div>
    );
} 

const HOC = (InnerComponent) => {
    return class HOCComponent extends PureComponent{
        render(){
            const { username } = this.props;
            return(
                <InnerComponent username = { username } />
            );
        }
    }
}

export default HOC(DisplayUser);

index.js:

import React from "react";
import ReactDOM from "react-dom";
import HOC from "./App";

ReactDOM.render(<HOC username = "Mike" birth = "1999-09-09" />,document.getElementById("root"));

HOC 函数接受一个组件作为参数,返回一个新的组件,新组件中渲染了参数组件,这样就可以对参数组件做一些配置,例如本例的过滤 props,只传递给参数组件合法的 props,不传递非法的 props。

高阶组件的分类

高阶组件分为两种类型:

上面的例子中的高阶组件类型就是代理型高阶组件。
二者的主要区别是对参数组件的操作上,代理型高阶组件是返回一个新的组件类,该类继承于根组件(Component 或 PureComponent),然后让这个新的组件类去渲染参数组件,这样就可以对参数组件实现包装或代理 props 的操作。继承型高阶组件也是返回一个新的组件类,但这个新的类不继承根组件类,而是继承于参数组件类,这样我们就可以重写参数组件类中的一些方法。
这里的高阶组件应该叫做高阶组件生成函数,其执行的返回值才叫高阶组件,为了方便我们这里统一称为高阶组件了,请知悉。
下面分别来说下这两类的高阶组件。

代理型高阶组件

代理型高阶组件接受一个组件作为参数,返回一个新的继承于根组件(Component、PureComponent)的组件,并用该新组件渲染参数组件,其基本结构为:

export const HOC = (SomeComponent) => {
    return class HOCComponent extends PureComponent{
        render(){
            doSomeThing()...
            return(
                <SomeComponent />
            );
        }
    }
}

代理型高阶组件有以下几个作用:

代理 props

代理型高阶组件可以接收被包装组件的 props,并对这些 props 进行操作,如增删改操作。在渲染被包装组件时,传入被修改的 props,如本文开头的例子。
基本结构如下:

export const HOC = (SomeComponent) => {
    return class HOCComponent extends PureComponent{
        render(){
            const newProps = doSomeThing(this.props);
            return(
                <SomeComponent {...newProps} />
            );
        }
    }
}

访问 ref

ref 是一个特殊的属性,可以声明为函数,如果 ref 属性是一个函数,那么将在组件装载完成后自动执行该函数,并将当前组件的实例传入该函数中
看一个栗子:

class SubComponent extends PureComponent{
    render(){
        return(
            <div className = "box">
                我想要被获取!!!
            </div>
        );
    }
}

const HOC = (InnerComponent) => {
    return class HOCComponent extends PureComponent{
        constructor(...args){
            super(...args);
            this.handSubRef = this.handSubRef.bind(this);
        }
        
        // 获取子组件的实例
        // 该函数在子组件被装载后自动调用
        handSubRef(subEle){
            console.log(subEle);
        }

        render(){
            return(
                <InnerComponent ref = { this.handSubRef } />
            );
        }
    }
}

export default HOC(SubComponent);

这样,在参数组件被装载后,就会自动执行 handSubRef 函数,并将参数组件的实例传入 handSubRef 函数。在 handSubRef 中可以获取参数组件的一些列属性,如 state、props 等。
注意,如果参数组件是一个无状态组件,那么传入 handSubRef 的参数将是 null

const SubComponent = (props) => {
    return(
        <div className = "box">
            我想要被获取!!!
        </div>
    );
} 

const HOC = (InnerComponent) => {
    return class HOCComponent extends PureComponent{
        constructor(...args){
            super(...args);
            this.handSubRef = this.handSubRef.bind(this);
        }
        
        // 参数组件是无状态组件
        // subEle 为 null
        handSubRef(subEle){
            console.log(subEle);
        }

        render(){
            return(
                <InnerComponent ref = { this.handSubRef } />
            );
        }
    }
}

export default HOC(SubComponent);

抽取状态

当一个组件的功能较复杂时,我们建议将组件拆分为容器组件和UI组件,UI组件负责展示,容器组件用来进行逻辑管理。这个步骤就是“抽取状态”。
我们将前面的计算器应用中的组件进行一些修改:

...
const UICounter = (props) => {
    const { increase,decrease,value} = props;
    return(
        <div className = "counter">
            <div>
                <button onClick = { increase }>+</button>
                <button onClick = { decrease }>-</button>
            </div>
            <span>当前的值为:{ value }</span>
        </div>
    );
}

const HOC = (SubEle) => {
    return class HOCComponent extends PureComponent{
        constructor(props) {
            super(props);
            // 获取初始状态
            this.state = {
                value:store.getState().value,
            };
        }

        componentWillMount() {
            // 监听 store 变化
            store.subscribe(this.watchStore.bind(this));
        }

        componentWillUnmount() {
            // 对 store 变化取消监听
            store.unsubscribe(this.watchStore.bind(this));
        }

        // 监听回调函数,当 store 变化后执行
        watchStore(){
            // 回调函数中重新设置状态
            this.setState(this.getCurrentState());
        }

        // 从 store 中获取状态
        getCurrentState(){
            return{
                value:store.getState().value,
            }
        }

        // 增加函数
        increase(){
            // 派发 INCREMENT Action
            store.dispatch(ACTIONS.increament());
        }

        // 减少函数
        decrease(){
            // 派发 DECREAMENT Action
            store.dispatch(ACTIONS.decreament());
        }

        render(){
            return(
                <UICounter
                    increase = { this.increase.bind(this)}
                    decrease = { this.decrease.bind(this)}
                    value = { this.state.value }
                />
            );
        }
    }
}

export default HOC(UICounter);
...

包装组件

包装组件就是对某些组件进行组合,返回不同的组合形式,同时可以设置展示样式。如将展示性 select 和输入表单包装成可搜索的 select等。
基本结构如下:

// 可以传入一个数组,或者单个组件
const HOC = (SubComponents) => {
    return class HOCComponent extends PureComponent{
        render(){
            const style = ...
            // 将子组件组合成一个大组件,同时可以修改样式
            return(
                <div style = { style }>
                    {...SubComponents}
                </div>
            );
        }
    }
}

export default HOC([Select,SearchBox]);

至此,代理型高阶组件就说完了,下面说说继承型高阶组件。

继承型高阶组件

和代理型高阶组件创建一个继承于根组件(Component、PureComponent)的组件类不同的是,继承型高阶组件是创建一个继承于参数组件的组件类,以此可以覆写父组件中的一些方法。
最常用的应用是重写父组件的生命周期函数:


const HOC = (ParComponents) => {
    return class HOCComponent extends ParComponents{
        // 覆写父组件的生命周期函数
        shouldComponentUpdate(nextProps, nextState) {
            ...
        }

        render(){
            // 渲染时仍然按照父组件的 render 函数进行渲染
            return super.render();
        }
    }
}

export default HOC(ParComponents);

子组件覆写了父组件的生命周期方法,以获取更好的渲染性能,在渲染(render)时,还是调用父类的 render 方法。

函数作为子组件

前面用高阶组件实现了代理 props 功能,对于特定的组件,需要传入特定的 props 名,由于每个组件需要的 props 可能有差异,如果使用高阶组件对参数组件进行判断,再传入合适的 props 名显然太麻烦了。这时如果我们需要更通用的方案,可以接收函数作为子组件,利用函数的形参的特性来排除组件 props 名之间的差异性。看一下演示代码:

...
// 此组件需要使用 user props
const SubComponent1 = (user) => {
    return(
        <TestComponent user = { user } />
    );
} 

// 此组件需要使用 username props
const SubComponent2 = (user) => {
    return(
        <TestComponent2 username = { user } />
    );
} 

// 高阶组件接受 testUser 作为 props
const HOC = (props) => {
    const { testUser } = props;
    return(
        props.children(testUser)
    );
}
...

使用此高阶组件传递 props:

...
<HOC testUser = "MIKE">
    { SubComponent1 }
</HOC>

<HOC testUser = "JACK">
    { SubComponent2 }
</HOC>
...

高阶组件接收一个函数作为子组件,然后调用这个组件函数,并传入相应的 props,函数调用的结果返回一个组件,组件需要接收什么样的 props 名可以自行定义,不用在高阶组件中进行判断了。
以函数作为组件的形式主要用于不同的组件需要接收同一份 props,但是它们需要的 props 名称不一样的情况,可以用函数的形参巧妙的化解掉父组件的判断。虽然用途较少,但不失为一个优秀的模式,将 children 进行调用简直是神来之笔有木有。

总结

本文谈到了 React 中的高阶组件,高阶组件主要包括两种类型:

代理型高阶组件的主要用途是:

继承型高阶组件的主要用途是覆写父组件的生命周期方法,通常是 shouldComponentUpdate 方法,以此来提高渲染性能,渲染时调用父类的 render 函数(super.render())。
最后,介绍了函数作为子组件的情形,主要用于不同的组件接受同一份 props,但是它们各自需要的 props 名不同的情况,利用函数的形参巧妙化解父组件的额外判断操作。这是一种优秀的模式,让人耳目一新。

完。

上一篇 下一篇

猜你喜欢

热点阅读