React - 高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

高阶函数与高阶组件:

如果一个函数 接受一个或多个函数作为参数或者返回一个函数 就可称之为 高阶函数。

function withGreeting(greeting = () => {}) {
    return greeting;
}

高阶组件是参数为组件,并且返回值为新组件的一类函数。
它们都是一个函数。

function HigherOrderComponent(WrappedComponent) {
    return ;
}

当高阶组件中返回的组件是 无状态组件(函数组件)时,该高阶组件其实就是一个 高阶函数,因为 无状态组件 本身就是一个纯函数。

React 中的高阶组件主要有两种形式:属性代理 和 反向继承。

属性代理(Props Proxy):

简单例子:

// 无状态
function HigherOrderComponent(WrappedComponent) {
    return props => ;
}
// 有状态
function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            return ;
        }

    };
}

对于有状态属性代理组件来说,其实就是 一个函数接受一个 WrappedComponent 组件作为参数传入,并返回一个继承了 React.Component 组件的类,且在该类的 render() 方法中返回被传入的 WrappedComponent 组件。

在有状态的属性代理高阶组件中可以进行一下操作:

  • 操作 props
    为 WrappedComponent 添加新的属性:
function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            const newProps = {
                name: '天空明朗',
                age: 12,
            };
            return ;
        }
    };
}
  • 抽离 state
    在返回组件中定义state和处理方法,通过props形式传递给参数组件,将参数组件中的state抽离到返回组件中做处理。
function withOnChange(WrappedComponent) {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                value: '',
            };
        }
        onChange = (e) => {
            let value = e.target.value;
            this.setState({
                vaule,
            });
        }
        render() {
            const newProps = {
                value: this.state.name,
                onChange: this.onChange,
            };
            return ;
        }
    };
}

const NameInput = props => ();
export default withOnChange(NameInput);

这样就将 input 转化成受控组件了。

  • 用其他元素包裹传入的组件 WrappedComponent
    给 WrappedComponent 组件包一层背景色:
function withBackgroundColor(WrappedComponent) {
    return class extends React.Component {
        render() {
            return (
                
); } }; }
反向继承(Inheritance Inversion):

简单例子:

function HigherOrderComponent(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            return super.render();
        }
    };
}

反向继承其实就是 一个函数接收一个 WrappedComponent 组件作为参数,并返回一个继承了参数 WrappedComponent 组件的类,且在该类的 render() 方法中返回 super.render() 方法。
属性代理中继承的是 React.Component,反向继承中继承的是传入的组件 WrappedComponent。

反向代理可以进行以下操作:

  • 操作state
    可以拿到 props 和 state 添加额外的元素
function withLogging(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            return (
                

state:

{JSON.stringify(this.state)}

props:

{JSON.stringify(this.props)}
{super.render()}
); } }; }
  • 渲染劫持(Render Highjacking)
    条件渲染:通过 props.isLoading 这个条件来判断渲染哪个组件。
function withLoading(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            if(this.props.isLoading) {
                return ;
            } else {
                return super.render();
            }
        }
    };
}
高阶组件存在的问题:
  • 静态方法丢失
    因为原始组件被包裹于一个容器组件内,也就意味着新组件会没有原始组件的任何静态方法:
// 定义静态方法
WrappedComponent.staticMethod = function() {}

// 使用高阶组件

const EnhancedComponent = HigherOrderComponent(WrappedComponent);

// 增强型组件没有静态方法
typeof EnhancedComponent.staticMethod === 'undefined' // true

对静态方法进行拷贝:

function HigherOrderComponent(WrappedComponent) {
    class Enhance extends React.Component {}
    // 必须得知道要拷贝的方法
    Enhance.staticMethod = WrappedComponent.staticMethod;
    return Enhance;
}

但是这么做的一个缺点就是必须知道要拷贝的方法是什么,不过 React 社区实现了一个库 hoist-non-react-statics 来自动处理,它会 自动拷贝所有非 React 的静态方法:

import hoistNonReactStatic from 'hoist-non-react-statics';
function HigherOrderComponent(WrappedComponent) {
    class Enhance extends React.Component {}
    hoistNonReactStatic(Enhance, WrappedComponent);
    return Enhance;
}
  • refs 属性不能透传
    一般来说高阶组件可以传递所有的 props 给包裹的组件 WrappedComponent,但是有一种属性不能传递,它就是 ref。与其他属性不同的地方在于 React 对其进行了特殊的处理。
    如果你向一个由高阶组件创建的组件的元素添加 ref 引用,那么 ref 指向的是最外层容器组件实例的,而不是被包裹的 WrappedComponent 组件。
    可以通过React 16.3中的一个名为 React.forwardRef 的 API 来解决这一问题:
function withLogging(WrappedComponent) {
    class Enhance extends WrappedComponent {
        render() {
            const {forwardedRef, ...rest} = this.props;
            // 把 forwardedRef 赋值给 ref
            return ;
        }
    };
    // React.forwardRef 方法会传入 props 和 ref 两个参数给其回调函数

    // 所以这边的 ref 是由 React.forwardRef 提供的
    function forwardRef(props, ref) {
        return 
    }
    return React.forwardRef(forwardRef);
}

const EnhancedComponent = withLogging(SomeComponent);
  • 反向继承不能应用于函数组件的解析
    反向继承的渲染劫持可以控制 WrappedComponent 的渲染过程,也就是说这个过程中我们可以对 elements tree、state、props 或 render() 的结果做各种操作。但函数组件中不存在super.render()、state等功能。
高阶组件的约定
  • props 保持一致
    高阶组件在为子组件添加特性的同时,要尽量保持原有组件的 props 不受影响,也就是说传入的组件和返回的组件在 props 上尽量保持一致。
  • 你不能在函数式(无状态)组件上使用 ref 属性,因为它没有实例
  • 不要以任何方式改变原始组件 WrappedComponent
function withLogging(WrappedComponent) {
    WrappedComponent.prototype.componentWillReceiveProps = function(nextProps) {
        console.log('Current props', this.props);
        console.log('Next props', nextProps);
    }
    return WrappedComponent;
}

const EnhancedComponent = withLogging(SomeComponent);

会发现在高阶组件的内部对 WrappedComponent 进行了修改,一旦对原组件进行了修改,那么就失去了组件复用的意义,所以请通过 纯函数(相同的输入总有相同的输出) 返回新的组件

function withLogging(WrappedComponent) {
    return class extends React.Component {
        componentWillReceiveProps() {
            console.log('Current props', this.props);
            console.log('Next props', nextProps);
        }

        render() {
            // 透传参数,不要修改它
            return ;
        }
    };
}
  • 将返回组件接收的 props 给被包裹的组件 WrappedComponent
function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            return ;
        }
    };
}
  • 不要再 render() 方法中使用高阶组件
class SomeComponent extends React.Component {
    render() {
        // 调用高阶函数的时候每次都会返回一个新的组件
        const EnchancedComponent = enhance(WrappedComponent);
        // 每次 render 的时候,都会使子对象树完全被卸载和重新
        // 重新加载一个组件会引起原有组件的状态和它的所有子组件丢失
        return ;
    }
}
  • 使用 compose 组合高阶组件
// 不要这么使用
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent));
// 可以使用一个 compose 函数组合这些高阶组件
// lodash, redux, ramda 等第三方库都提供了类似 `compose` 功能的函数
const enhance = compose(withRouter, connect(commentSelector));
const EnhancedComponent = enhance(WrappedComponent);

因为按照 约定 实现的高阶组件其实就是一个纯函数,如果多个函数的参数一样(在这里 withRouter 函数和 connect(commentSelector)所返回的函数所需的参数都是 WrappedComponent),所以就可以通过 compose 方法来组合这些函数。

高阶组件的应用场景:
  • 权限控制:
    利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别 和 页面元素级别,这里以页面级别来举一个例子:
// HOC.js
function withAdminAuth(WrappedComponent) {
    return class extends React.Component {
        state = {
            isAdmin: false,
        }
        async componentWillMount() {
            const currentRole = await getCurrentUserRole();
            this.setState({
                isAdmin: currentRole === 'Admin',
            });
        }

        render() {
           if (this.state.isAdmin) {
                return ;
            } else {
                return (
您没有权限查看该页面,请联系管理员!
); } } }; }

然后是两个页面:

// pages/page-a.js
class PageA extends React.Component {
    constructor(props) {
        super(props);
        // something here...
    }
    componentWillMount() {
        // fetching data
    }
    render() {
        // render page with data
    }
}
export default withAdminAuth(PageA);

// pages/page-b.js
class PageB extends React.Component {
    constructor(props) {
        super(props);
        // something here...
    }
    componentWillMount() {
        // fetching data
    }
    render() {
        // render page with data
    }
}
export default withAdminAuth(PageB);

使用高阶组件对代码进行复用之后,可以非常方便的进行拓展,比如产品经理说,PageC 页面也要有 Admin 权限才能进入,我们只需要在 pages/page-c.js 中把返回的 PageC 嵌套一层 withAdminAuth 高阶组件就行,就像这样 withAdminAuth(PageC)。是不是非常完美!非常高效!!但是。。第二天产品经理又说,PageC 页面只要 VIP 权限就可以访问了。你三下五除二实现了一个高阶组件 withVIPAuth。

其实你还可以更高效的,就是在高阶组件之上再抽象一层,无需实现各种 withXXXAuth 高阶组件,因为这些高阶组件本身代码就是高度相似的,所以我们要做的就是实现一个 返回高阶组件的函数,把 变的部分(Admin、VIP) 抽离出来,保留 不变的部分,具体实现如下:

// HOC.js
const withAuth = role => WrappedComponent => {
    return class extends React.Component {
        state = {
            permission: false,
        }
        async componentWillMount() {
            const currentRole = await getCurrentUserRole();
            this.setState({
                permission: currentRole === role,
            });
        }

        render() {
            if (this.state.permission) {
                return ;
            } else {
                return (
您没有权限查看该页面,请联系管理员!
); } } }; } withAuth(‘Admin’)(PageA);

有没有发现和 react-redux 的 connect 方法的使用方式非常像?没错,connect 其实也是一个 返回高阶组件的函数。

  • 页面复用
    假设我们有两个页面 pageA 和 pageB 分别渲染两个分类的电影列表,普通写法可能是这样:
// pages/page-a.js
class PageA extends React.Component {
    state = {
        movies: [],
    }
    // ...
    async componentWillMount() {
        const movies = await fetchMoviesByType('science-fiction');
        this.setState({
            movies,
        });
    }
    render() {
        return 
    }
}
export default PageA;

// pages/page-b.js
class PageB extends React.Component {
    state = {
        movies: [],
    }
    // ...
    async componentWillMount() {
        const movies = await fetchMoviesByType('action');
        this.setState({
            movies,

        });
    }
    render() {
        return 
    }
}
export default PageB;

页面少的时候可能没什么问题,但是假如随着业务的进展,需要上线的越来越多类型的电影,就会写很多的重复代码,所以我们需要重构一下:

const withFetching = fetching => WrappedComponent => {
    return class extends React.Component {
        state = {
            data: [],
        }
        async componentWillMount() {
            const data = await fetching();
            this.setState({
                data,
            });
        }
        render() {
        return ;
        }
    }
}

// pages/page-a.js
export default withFetching(fetching('science-fiction'))(MovieList);

// pages/page-b.js
export default withFetching(fetching('action'))(MovieList);

// pages/page-other.js
export default withFetching(fetching('some-other-type'))(MovieList);

会发现 withFetching 其实和前面的 withAuth 函数类似,把 变的部分(fetching(type)) 抽离到外部传入,从而实现页面的复用。

装饰器模式:

高阶组件其实就是装饰器模式在 React 中的实现:通过给函数传入一个组件(函数或类)后在函数内部对该组件(函数或类)进行功能的增强(不修改传入参数的前提下),最后返回这个组件(函数或类),即允许向一个现有的组件添加新的功能,同时又不去修改该组件,属于 包装模式(Wrapper Pattern) 的一种。

什么是装饰者模式:在不改变对象自身的前提下在程序运行期间动态的给对象添加一些额外的属性或行为。

相比于使用继承,装饰者模式是一种更轻便灵活的做法。

总结:
  • 高阶组件不是组件,是一个把某个组件转换成另一个组件的函数
  • 高阶组件的主要作用是代码复用
  • 高阶组件是装饰器模式在React中的实现

参考链接:React 中的高阶组件及其应用场景

你可能感兴趣的:(React - 高阶组件)