精华内容
下载资源
问答
  • 1.什么是高阶组件 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。 具体而言,高阶组件是参数为组件,返回值为新...

    1.什么是高阶组件

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

    具体而言,高阶组件是参数为组件,返回值为新组件的函数。

    2.例子

    高阶组件

    import React from 'react';
    
    export default function HocHour(Com) {
      return class extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            hour: 0,
          };
        }
    
        componentDidMount() {
          const p = new Promise(this.getFirstHour);
          let num = 0;
          p.then(res => {
            num += res;
            return new Promise(this.getSecondHour);
          }).then(res => {
            num += res;
            this.setState({ hour: num })
          })
        }
    
        getFirstHour = (resolve, reject) => {
          setTimeout(() => {
            resolve(3);
          }, 1000);
        }
    
        getSecondHour = (resolve, reject) => {
          setTimeout(() => {
            resolve(5);
          }, 1000);
        }
    
        render() {
          return <Com {...this.state} />
        }
      }
    }
    
    

    class组件

    import React, { Component } from 'react';
    import HocHover from './components/HocHour';
    
    class Index extends Component {
      constructor(props) {
        super(props);
        this.state = {
          number1: 1,
          number2: 2,
        }
      }
    
      componentDidMount() {
      }
    
      render() {
        const { hour } = this.props;
        const { number1, number2 } = this.state;
    
        return <div style={{ padding: '20px' }}>
          <div>{hour}</div>
          <div>{hour + number1}</div>
          <div>{hour + number2}</div>
        </div>
      }
    }
    
    export default HocHover(Index);
    

    上面的例子是 在HocHover高阶组件中通过setTimeout模拟接口调用得到的hour值,在Index组件中各个地方使用到。最终在页面两秒后显示如下图:
    在这里插入图片描述

    总结

    千山万水总有情,点赞再走行不行!

    展开全文
  • React高阶组件以及应用场景

    千次阅读 2019-09-12 09:21:28
    什么是高阶组件 在解释什么是高阶组件之前,可以先了解一下什么是 高阶函数,因为它们的概念非常相似,下面是 高阶函数 的定义: 如果一个函数 接受一个或多个函数作为参数或者返回一个函数 就可称之为 高阶函数。...

    什么是高阶组件

    在解释什么是高阶组件之前,可以先了解一下什么是 高阶函数,因为它们的概念非常相似,下面是 高阶函数 的定义:

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

    下面就是一个简单的高阶函数:

    function withGreeting(greeting = () => {}) {
        return greeting;
    }
    复制代码

    高阶组件 的定义和 高阶函数 非常相似:

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

    下面就是一个简单的高阶组件:

    function HigherOrderComponent(WrappedComponent) {
        return <WrappedComponent />;
    }
    复制代码

    所以你可能会发现,当高阶组件中返回的组件是 无状态组件(Stateless Component) 时,该高阶组件其实就是一个 高阶函数,因为 无状态组件 本身就是一个纯函数。

    无状态组件也称函数式组件。

    React 中的高阶组件

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

    属性代理(Props Proxy)

    最简单的属性代理实现:

    // 无状态
    function HigherOrderComponent(WrappedComponent) {
        return props => <WrappedComponent {...props} />;
    }
    // or
    // 有状态
    function HigherOrderComponent(WrappedComponent) {
        return class extends React.Component {
            render() {
                return <WrappedComponent {...this.props} />;
    
            }
    
        };
    }
    复制代码

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

    那我们可以利用属性代理类型的高阶组件做一些什么呢?

    因为属性代理类型的高阶组件返回的是一个标准的 React.Component 组件,所以在 React 标准组件中可以做什么,那在属性代理类型的高阶组件中就也可以做什么,比如:

    • 操作 props
    • 抽离 state
    • 通过 ref 访问到组件实例
    • 用其他元素包裹传入的组件 WrappedComponent

    操作 props

    为 WrappedComponent 添加新的属性:

    function HigherOrderComponent(WrappedComponent) {
    
        return class extends React.Component {
    
            render() {
    
                const newProps = {
    
                    name: '大板栗',
                    age: 18,
                };
                return <WrappedComponent {...this.props} {...newProps} />;
            }
        };
    }
    复制代码

    抽离 state

    利用 props 和回调函数把 state 抽离出来:

    function withOnChange(WrappedComponent) {
        return class extends React.Component {
            constructor(props) {
                super(props);
                this.state = {
                    name: '',
                };
            }
    
            onChange = () => {
                this.setState({
                    name: '大板栗',
                });
            }
    
            render() {
                const newProps = {
                    name: {
                        value: this.state.name,
                        onChange: this.onChange,
                    },
                };
                return <WrappedComponent {...this.props} {...newProps} />;
            }
        };
    
    }
    复制代码

    如何使用:

    const NameInput = props => (<input name="name" {...props.name} />);
    export default withOnChange(NameInput);
    复制代码

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

    通过 ref 访问到组件实例

    有时会有需要访问 DOM element (使用第三方 DOM 操作库)的时候就会用到组件的 ref 属性。它只能声明在 Class 类型的组件上,而无法声明在函数(无状态)类型的组件上。

    ref 的值可以是字符串(不推荐使用)也可以是一个回调函数,如果是回调函数的话,它的执行时机是:

    • 组件被挂载后(componentDidMount),回调函数立即执行,回调函数的参数为该组件的实例。
    • 组件被卸载(componentDidUnmount)或者原有的 ref 属性本身发生变化的时候,此时回调函数也会立即执行,且回调函数的参数为 null。

    如何在 高阶组件 中获取到 WrappedComponent 组件的实例呢?答案就是可以通过 WrappedComponent 组件的 ref 属性,该属性会在组件 componentDidMount 的时候执行 ref 的回调函数并传入该组件的实例:

    function HigherOrderComponent(WrappedComponent) {
        return class extends React.Component {
            executeInstanceMethod = (wrappedComponentInstance) => {
                wrappedComponentInstance.someMethod();
            }
    
            render() {
                return <WrappedComponent {...this.props} ref={this.executeInstanceMethod} />;
            }
        };
    }
    复制代码

    注意:不能在无状态组件(函数类型组件)上使用 ref 属性,因为无状态组件没有实例。

    用其他元素包裹传入的组件 WrappedComponent

    给 WrappedComponent 组件包一层背景色为 #fafafa 的 div 元素:

    function withBackgroundColor(WrappedComponent) {
        return class extends React.Component {
            render() {
                return (
                    <div style={{ backgroundColor: '#fafafa' }}>
                        <WrappedComponent {...this.props} {...newProps} />
                    </div>
                );
            }
        };
    }
    复制代码

    反向继承(Inheritance Inversion)

    最简单的反向继承实现:

    function HigherOrderComponent(WrappedComponent) {
        return class extends WrappedComponent {
            render() {
                return super.render();
            }
        };
    }
    复制代码

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

    会发现其属性代理和反向继承的实现有些类似的地方,都是返回一个继承了某个父类的子类,只不过属性代理中继承的是 React.Component,反向继承中继承的是传入的组件 WrappedComponent。

    反向继承可以用来做什么:

    • 操作 state
    • 渲染劫持(Render Highjacking)

    操作 state

    高阶组件中可以读取、编辑和删除 WrappedComponent 组件实例中的 state。甚至可以增加更多的 state 项,但是 非常不建议这么做因为这可能会导致 state 难以维护及管理。

    function withLogging(WrappedComponent) {
        return class extends WrappedComponent {
            render() {
                return (
                    <div>
                        <h2>Debugger Component Logging...</h2>
                        <p>state:</p>
                        <pre>{JSON.stringify(this.state, null, 4)}</pre>
                        <p>props:</p>
                        <pre>{JSON.stringify(this.props, null, 4)}</pre>
                        {super.render()}
                    </div>
                );
            }
        };
    }
    复制代码

    在这个例子中利用高阶函数中可以读取 state 和 props 的特性,对 WrappedComponent 组件做了额外元素的嵌套,把 WrappedComponent 组件的 state 和 props 都打印了出来.

    渲染劫持(Render Highjacking)

    之所以称之为 渲染劫持 是因为高阶组件控制着 WrappedComponent 组件的渲染输出,通过渲染劫持我们可以:

    • 有条件地展示元素树(element tree)
    • 操作由 render() 输出的 React 元素树
    • 在任何由 render() 输出的 React 元素中操作 props
    • 用其他元素包裹传入的组件 WrappedComponent (同 属性代理)

    条件渲染

    通过 props.isLoading 这个条件来判断渲染哪个组件。

    function withLoading(WrappedComponent) {
        return class extends WrappedComponent {
            render() {
                if(this.props.isLoading) {
                    return <Loading />;
                } else {
                    return super.render();
                }
            }
        };
    }
    复制代码

    操作由 render() 输出的 React 元素树

    function HigherOrderComponent(WrappedComponent) {
        return class extends WrappedComponent {
            render() {
                const tree = super.render();
                const newProps = {};
                if (tree && tree.type === 'input') {
                    newProps.value = 'something here';
                }
    
                const props = {
                    ...tree.props,
                  ...newProps,
                };
    
                const newTree = React.cloneElement(tree, props, tree.props.children);
                return newTree;
            }
        };
    }
    复制代码

    高阶组件存在的问题

    • 静态方法丢失
    • refs 属性不能透传
    • 反向继承不能保证完整的子组件树被解析

    静态方法丢失

    因为原始组件被包裹于一个容器组件内,也就意味着新组件会没有原始组件的任何静态方法:

    // 定义静态方法
    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 组件。

    那如果有一定要传递 ref 的需求呢,别急,React 为我们提供了一个名为 React.forwardRef 的 API 来解决这一问题(在 React 16.3 版本中被添加):

    function withLogging(WrappedComponent) {
        class Enhance extends WrappedComponent {
            componentWillReceiveProps() {
                console.log('Current props', this.props);
                console.log('Next props', nextProps);
            }
    
            render() {
                const {forwardedRef, ...rest} = this.props;
                // 把 forwardedRef 赋值给 ref
                return <WrappedComponent {...rest} ref={forwardedRef} />;
            }
        };
        // React.forwardRef 方法会传入 props 和 ref 两个参数给其回调函数
    
        // 所以这边的 ref 是由 React.forwardRef 提供的
        function forwardRef(props, ref) {
            return <Enhance {...props} forwardRef={ref} />
        }
        return React.forwardRef(forwardRef);
    }
    
    const EnhancedComponent = withLogging(SomeComponent);
    复制代码

    反向继承不能保证完整的子组件树被解析

    React 组件有两种形式,分别是 class 类型和 function 类型(无状态组件)。

    我们知道反向继承的渲染劫持可以控制 WrappedComponent 的渲染过程,也就是说这个过程中我们可以对 elements tree、state、props 或 render() 的结果做各种操作。

    但是如果渲染 elements tree 中包含了 function 类型的组件的话,这时候就不能操作组件的子组件了。

    高阶组件的约定

    高阶组件带给我们极大方便的同时,我们也要遵循一些约定:

    • props 保持一致
    • 你不能在函数式(无状态)组件上使用 ref 属性,因为它没有实例
    • 不要以任何方式改变原始组件 WrappedComponent
    • 透传不相关 props 属性给被包裹的组件 WrappedComponent
    • 不要再 render() 方法中使用高阶组件
    • 使用 compose 组合高阶组件
    • 包装显示名字以便于调试

    props 保持一致

    高阶组件在为子组件添加特性的同时,要尽量保持原有组件的 props 不受影响,也就是说传入的组件和返回的组件在 props 上尽量保持一致。

    你不能在函数式(无状态)组件上使用 ref 属性,因为它没有实例

    函数式组件本身没有实例,所以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 <WrappedComponent {...this.props} />;
            }
        };
    }
    复制代码

    这样优化之后的 withLogging 是一个 纯函数,并不会修改 WrappedComponent 组件,所以不需要担心有什么副作用,进而达到组件复用的目的。

    透传不相关 props 属性给被包裹的组件 WrappedComponent

    function HigherOrderComponent(WrappedComponent) {
        return class extends React.Component {
            render() {
                return <WrappedComponent name="name" {...this.props} />;
            }
        };
    }
    复制代码

    不要再 render() 方法中使用高阶组件

    class SomeComponent extends React.Component {
        render() {
            // 调用高阶函数的时候每次都会返回一个新的组件
            const EnchancedComponent = enhance(WrappedComponent);
            // 每次 render 的时候,都会使子对象树完全被卸载和重新
            // 重新加载一个组件会引起原有组件的状态和它的所有子组件丢失
            return <EnchancedComponent />;
        }
    }
    复制代码

    使用 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 方法来组合这些函数。

    使用 compose 组合高阶组件使用,可以显著提高代码的可读性和逻辑的清晰度。

    包装显示名字以便于调试

    高阶组件创建的容器组件在 React Developer Tools 中的表现和其它的普通组件是一样的。为了便于调试,可以选择一个显示名字,传达它是一个高阶组件的结果。

    const getDisplayName = WrappedComponent => WrappedComponent.displayName || WrappedComponent.name || 'Component';
    function HigherOrderComponent(WrappedComponent) {
        class HigherOrderComponent extends React.Component {/* ... */}
        HigherOrderComponent.displayName = `HigherOrderComponent(${getDisplayName(WrappedComponent)})`;
        return HigherOrderComponent;
    }
    复制代码

    实际上 recompose 库实现了类似的功能,懒的话可以不用自己写:

    import getDisplayName from 'recompose/getDisplayName';
    HigherOrderComponent.displayName = `HigherOrderComponent(${getDisplayName(BaseComponent)})`;
    // Or, even better:
    import wrapDisplayName from 'recompose/wrapDisplayName';
    HigherOrderComponent.displayName = wrapDisplayName(BaseComponent, 'HigherOrderComponent');
    复制代码

    高阶组件的应用场景

    不谈场景的技术就是在耍流氓,所以接下来说一下如何在业务场景中使用高阶组件。

    权限控制

    利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别 和 页面元素级别,这里以页面级别来举一个栗子:

    // 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 <WrappedComponent {...this.props} />;
                } else {
                    return (<div>您没有权限查看该页面,请联系管理员!</div>);
                }
            }
        };
    }
    复制代码

    然后是两个页面:

    // 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 <WrappedComponent {...this.props} />;
                } else {
                    return (<div>您没有权限查看该页面,请联系管理员!</div>);
                }
            }
        };
    }
    复制代码

    可以发现经过对高阶组件再进行了一层抽象后,前面的 withAdminAuth 可以写成 withAuth(‘Admin’) 了,如果此时需要 VIP 权限的话,只需在 withAuth 函数中传入 ‘VIP’ 就可以了。

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

    组件渲染性能追踪

    借助父组件子组件生命周期规则捕获子组件的生命周期,可以方便的对某个组件的渲染时间进行记录:

    class Home extends React.Component {
        render() {
            return (<h1>Hello World.</h1>);
        }
    }
    
    function withTiming(WrappedComponent) {
    
        return class extends WrappedComponent {
            constructor(props) {
                super(props);
                this.start = 0;
                this.end = 0;
            }
            componentWillMount() {
                super.componentWillMount && super.componentWillMount();
                this.start = Date.now();
            }
            componentDidMount() {
                super.componentDidMount && super.componentDidMount();
                this.end = Date.now();
                console.log(`${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`);
            }
            render() {
                return super.render();
            }
        };
    }
    export default withTiming(Home);
    复制代码

    withTiming 是利用 反向继承 实现的一个高阶组件,功能是计算被包裹组件(这里是 Home 组件)的渲染时间。

    页面复用

    假设我们有两个页面 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 <MovieList movies={this.state.movies} />
        }
    }
    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 <MovieList movies={this.state.movies} />
        }
    }
    
    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 <WrappedComponent data={this.state.data} {...this.props} />;
            }
        }
    }
    
    // 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)) 抽离到外部传入,从而实现页面的复用。

    装饰者模式?高阶组件?AOP?

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

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

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

    使用装饰者模式实现 AOP:

    面向切面编程(AOP)和面向对象编程(OOP)一样,只是一种编程范式,并没有规定说要用什么方式去实现 AOP。

    // 在需要执行的函数之前执行某个新添加的功能函数
    Function.prototype.before = function(before = () => {}) {
        return () => {
            before.apply(this, arguments);
            return this.apply(this, arguments);
    
        };
    }
    // 在需要执行的函数之后执行某个新添加的功能函数
    Function.prototype.after = function(after = () => {}) {
        return () => {
            const result = after.apply(this, arguments);
            this.apply(this, arguments);
            return result;
        };
    }
    复制代码

    可以发现其实 before 和 after 就是一个 高阶函数,和高阶组件非常类似。

    面向切面编程(AOP)主要应用在 与核心业务无关但又在多个模块使用的功能比如权限控制、日志记录、数据校验、异常处理、统计上报等等领域。

    类比一下 AOP 你应该就知道高阶组件通常是处理哪一类型的问题了吧。

    总结

    React 中的 高阶组件 其实是一个非常简单的概念,但又非常实用。在实际的业务场景中合理的使用高阶组件,可以提高代码的复用性和灵活性。

    最后的最后,再对高阶组件进行一个小小的总结:

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

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


    作者:冷星1024
    链接:https://juejin.im/post/5d7893d56fb9a06af92bcdbe
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    展开全文
  • 关键词:高阶函数、高阶组件、属性代理、反向继承、装饰器模式、受控组件本文目录:什么是高阶组件React 中的高阶组件属性代理(Props Proxy)反向继承(Inher...

    640?wx_fmt=png

    关键词:高阶函数、高阶组件、属性代理、反向继承、装饰器模式、受控组件

    本文目录:

    • 什么是高阶组件

    • React 中的高阶组件

      • 属性代理(Props Proxy)

      • 反向继承(Inheritance Inversion)

    • 高阶组件存在的问题

    • 高阶组件的约定

    • 高阶组件的应用场景

    • 装饰者模式?高阶组件?AOP?

    • 总结

    什么是高阶组件

    在解释什么是高阶组件之前,可以先了解一下什么是 高阶函数,因为它们的概念非常相似,下面是 高阶函数的定义:

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

    下面就是一个简单的高阶函数:

    function withGreeting(greeting = () =&gt; {}) {	
        return greeting;	
    }

    高阶组件 的定义和 高阶函数 非常相似:

    如果一个函数 接受一个或多个组件作为参数并且返回一个组件 就可称之为 高阶组件

    下面就是一个简单的高阶组件:

    function HigherOrderComponent(WrappedComponent) {	
        return &lt;WrappedComponent /&gt;;	
    }

    所以你可能会发现,当高阶组件中返回的组件是 无状态组件(Stateless Component) 时,该高阶组件其实就是一个 高阶函数,因为 无状态组件 本身就是一个纯函数。

    无状态组件也称函数式组件。

    React 中的高阶组件

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

    属性代理(Props Proxy)

    最简单的属性代理实现:

    // 无状态	
    function HigherOrderComponent(WrappedComponent) {	
        return props =&gt; &lt;WrappedComponent {...props} /&gt;;	
    }	
    // or	
    // 有状态	
    function HigherOrderComponent(WrappedComponent) {	
        return class extends React.Component {	
            render() {	
                return &lt;WrappedComponent {...this.props} /&gt;;	
            }	
        };	
    }

    可以发现,属性代理其实就是 一个函数接受一个 WrappedComponent 组件作为参数传入,并返回一个继承了 React.Component 组件的类,且在该类的 render() 方法中返回被传入的 WrappedComponent 组件

    那我们可以利用属性代理类型的高阶组件做一些什么呢?

    因为属性代理类型的高阶组件返回的是一个标准的 React.Component 组件,所以在 React 标准组件中可以做什么,那在属性代理类型的高阶组件中就也可以做什么,比如:

    • 操作 props

    • 抽离 state

    • 通过 ref 访问到组件实例

    • 用其他元素包裹传入的组件 WrappedComponent

    操作 props

    WrappedComponent 添加新的属性:

    function HigherOrderComponent(WrappedComponent) {	
        return class extends React.Component {	
            render() {	
                const newProps = {	
                    name: '大板栗',	
                    age: 18,	
                };	
                return &lt;WrappedComponent {...this.props} {...newProps} /&gt;;	
            }	
        };	
    }

    抽离 state

    利用 props 和回调函数把 state 抽离出来:

    function withOnChange(WrappedComponent) {	
        return class extends React.Component {	
            constructor(props) {	
                super(props);	
                this.state = {	
                    name: '',	
                };	
            }	
            onChange = () =&gt; {	
                this.setState({	
                    name: '大板栗',	
                });	
            }	
            render() {	
                const newProps = {	
                    name: {	
                        value: this.state.name,	
                        onChange: this.onChange,	
                    },	
                };	
                return &lt;WrappedComponent {...this.props} {...newProps} /&gt;;	
            }	
        };	
    }

    如何使用:

    const NameInput = props =&gt; (&lt;input name="name" {...props.name} /&gt;);	
    export default withOnChange(NameInput);

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

    通过 ref 访问到组件实例

    有时会有需要访问 DOM element (使用第三方 DOM 操作库)的时候就会用到组件的 ref 属性。它只能声明在 Class 类型的组件上,而无法声明在函数(无状态)类型的组件上。

    ref 的值可以是字符串(不推荐使用)也可以是一个回调函数,如果是回调函数的话,它的执行时机是:

    • 组件被挂载后( componentDidMount),回调函数立即执行,回调函数的参数为该组件的实例。

    • 组件被卸载( componentDidUnmount)或者原有的 ref 属性本身发生变化的时候,此时回调函数也会立即执行,且回调函数的参数为 null

    如何在 高阶组件 中获取到 WrappedComponent 组件的实例呢?答案就是可以通过 WrappedComponent 组件的 ref 属性,该属性会在组件 componentDidMount 的时候执行 ref 的回调函数并传入该组件的实例:

    function HigherOrderComponent(WrappedComponent) {	
        return class extends React.Component {	
            executeInstanceMethod = (wrappedComponentInstance) =&gt; {	
                wrappedComponentInstance.someMethod();	
            }	
            render() {	
                return &lt;WrappedComponent {...this.props} ref={this.executeInstanceMethod} /&gt;;	
            }	
        };	
    }

    注意:不能在无状态组件(函数类型组件)上使用 ref 属性,因为无状态组件没有实例。

    用其他元素包裹传入的组件 WrappedComponent

    WrappedComponent 组件包一层背景色为 #fafafadiv 元素:

    function withBackgroundColor(WrappedComponent) {	
        return class extends React.Component {	
            render() {	
                return (	
                    &lt;div style={{ backgroundColor: '#fafafa' }}&gt;	
                        &lt;WrappedComponent {...this.props} {...newProps} /&gt;	
                    &lt;/div&gt;	
                );	
            }	
        };	
    }

    反向继承(Inheritance Inversion)

    最简单的反向继承实现:

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

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

    会发现其属性代理和反向继承的实现有些类似的地方,都是返回一个继承了某个父类的子类,只不过属性代理中继承的是 React.Component,反向继承中继承的是传入的组件 WrappedComponent

    反向继承可以用来做什么:

    • 操作 state

    • 渲染劫持(Render Highjacking)

    操作 state

    高阶组件中可以读取、编辑和删除 WrappedComponent 组件实例中的 state。甚至可以增加更多的 state项,但是 非常不建议这么做 因为这可能会导致 state 难以维护及管理。

    function withLogging(WrappedComponent) {	
        return class extends WrappedComponent {	
            render() {	
                return (	
                    &lt;div&gt;	
                        &lt;h2&gt;Debugger Component Logging...&lt;/h2&gt;	
                        &lt;p&gt;state:&lt;/p&gt;	
                        &lt;pre&gt;{JSON.stringify(this.state, null, 4)}&lt;/pre&gt;	
                        &lt;p&gt;props:&lt;/p&gt;	
                        &lt;pre&gt;{JSON.stringify(this.props, null, 4)}&lt;/pre&gt;	
                        {super.render()}	
                    &lt;/div&gt;	
                );	
            }	
        };	
    }

    在这个例子中利用高阶函数中可以读取 stateprops 的特性,对 WrappedComponent 组件做了额外元素的嵌套,把 WrappedComponent 组件的 stateprops 都打印了出来,

    渲染劫持

    之所以称之为 渲染劫持 是因为高阶组件控制着 WrappedComponent 组件的渲染输出,通过渲染劫持我们可以:

    • 有条件地展示元素树( element tree

    • 操作由 render() 输出的 React 元素树

    • 在任何由 render() 输出的 React 元素中操作 props

    • 用其他元素包裹传入的组件 WrappedComponent (同 属性代理

    条件渲染

    通过 props.isLoading 这个条件来判断渲染哪个组件。

    function withLoading(WrappedComponent) {	
        return class extends WrappedComponent {	
            render() {	
                if(this.props.isLoading) {	
                    return &lt;Loading /&gt;;	
                } else {	
                    return super.render();	
                }	
            }	
        };	
    }
    修改由 render() 输出的 React 元素树

    修改元素树:

    function HigherOrderComponent(WrappedComponent) {	
        return class extends WrappedComponent {	
            render() {	
                const tree = super.render();	
                const newProps = {};	
                if (tree &amp;&amp; tree.type === 'input') {	
                    newProps.value = 'something here';	
                }	
                const props = {	
                    ...tree.props,	
                    ...newProps,	
                };	
                const newTree = React.cloneElement(tree, props, tree.props.children);	
                return newTree;	
            }	
        };	
    }

    高阶组件存在的问题

    • 静态方法丢失

    • refs 属性不能透传

    • 反向继承不能保证完整的子组件树被解析

    静态方法丢失

    因为原始组件被包裹于一个容器组件内,也就意味着新组件会没有原始组件的任何静态方法:

    // 定义静态方法	
    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 组件。

    那如果有一定要传递 ref 的需求呢,别急,React 为我们提供了一个名为 React.forwardRef 的 API 来解决这一问题(在 React 16.3 版本中被添加):

    function withLogging(WrappedComponent) {	
        class Enhance extends WrappedComponent {	
            componentWillReceiveProps() {	
                console.log('Current props', this.props);	
                console.log('Next props', nextProps);	
            }	
            render() {	
                const {forwardedRef, ...rest} = this.props;	
                // 把 forwardedRef 赋值给 ref	
                return &lt;WrappedComponent {...rest} ref={forwardedRef} /&gt;;	
            }	
        };	
        // React.forwardRef 方法会传入 props 和 ref 两个参数给其回调函数	
        // 所以这边的 ref 是由 React.forwardRef 提供的	
        function forwardRef(props, ref) {	
            return &lt;Enhance {...props} forwardRef={ref} /&gt;	
        }	
        return React.forwardRef(forwardRef);	
    }	
    const EnhancedComponent = withLogging(SomeComponent);

    反向继承不能保证完整的子组件树被解析

    React 组件有两种形式,分别是 class 类型和 function 类型(无状态组件)。

    我们知道反向继承的渲染劫持可以控制 WrappedComponent 的渲染过程,也就是说这个过程中我们可以对 elements treestatepropsrender() 的结果做各种操作。

    但是如果渲染 elements tree 中包含了 function 类型的组件的话,这时候就不能操作组件的子组件了。

    高阶组件的约定

    高阶组件带给我们极大方便的同时,我们也要遵循一些 约定

    • props 保持一致

    • 你不能在函数式(无状态)组件上使用 ref 属性,因为它没有实例

    • 不要以任何方式改变原始组件 WrappedComponent

    • 透传不相关 props 属性给被包裹的组件 WrappedComponent

    • 不要再 render() 方法中使用高阶组件

    • 使用 compose 组合高阶组件

    • 包装显示名字以便于调试

    props 保持一致

    高阶组件在为子组件添加特性的同时,要尽量保持原有组件的 props 不受影响,也就是说传入的组件和返回的组件在 props 上尽量保持一致。

    不要改变原始组件 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 &lt;WrappedComponent {...this.props} /&gt;;	
            }	
        };	
    }

    这样优化之后的 withLogging 是一个 纯函数,并不会修改 WrappedComponent 组件,所以不需要担心有什么副作用,进而达到组件复用的目的。

    透传不相关 props 属性给被包裹的组件 WrappedComponent

    function HigherOrderComponent(WrappedComponent) {	
        return class extends React.Component {	
            render() {	
                return &lt;WrappedComponent name="name" {...this.props} /&gt;;	
            }	
        };	
    }

    不要在 render() 方法中使用高阶组件

    class SomeComponent extends React.Component {	
        render() {	
            // 调用高阶函数的时候每次都会返回一个新的组件	
            const EnchancedComponent = enhance(WrappedComponent);	
            // 每次 render 的时候,都会使子对象树完全被卸载和重新	
            // 重新加载一个组件会引起原有组件的状态和它的所有子组件丢失	
            return &lt;EnchancedComponent /&gt;;	
        }	
    }

    使用 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 方法来组合这些函数。

    使用 compose 组合高阶组件使用,可以显著提高代码的可读性和逻辑的清晰度。

    包装显示名字以便于调试

    高阶组件创建的容器组件在 React Developer Tools 中的表现和其它的普通组件是一样的。为了便于调试,可以选择一个显示名字,传达它是一个高阶组件的结果。

    const getDisplayName = WrappedComponent =&gt; WrappedComponent.displayName || WrappedComponent.name || 'Component';	
    function HigherOrderComponent(WrappedComponent) {	
        class HigherOrderComponent extends React.Component {/* ... */}	
        HigherOrderComponent.displayName = `HigherOrderComponent(${getDisplayName(WrappedComponent)})`;	
        return HigherOrderComponent;	
    }

    实际上 recompose 库实现了类似的功能,懒的话可以不用自己写:

    import getDisplayName from 'recompose/getDisplayName';	
    HigherOrderComponent.displayName = `HigherOrderComponent(${getDisplayName(BaseComponent)})`;	
    // Or, even better:	
    import wrapDisplayName from 'recompose/wrapDisplayName';	
    HigherOrderComponent.displayName = wrapDisplayName(BaseComponent, 'HigherOrderComponent');

    高阶组件的应用场景

    不谈场景的技术就是在耍流氓,所以接下来说一下如何在业务场景中使用高阶组件。

    权限控制

    利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别页面元素级别,这里以页面级别来举一个栗子:

    // 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 &lt;WrappedComponent {...this.props} /&gt;;	
                } else {	
                    return (&lt;div&gt;您没有权限查看该页面,请联系管理员!&lt;/div&gt;);	
                }	
            }	
        };	
    }

    然后是两个页面:

    // 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 =&gt; WrappedComponent =&gt; {	
        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 &lt;WrappedComponent {...this.props} /&gt;;	
                } else {	
                    return (&lt;div&gt;您没有权限查看该页面,请联系管理员!&lt;/div&gt;);	
                }	
            }	
        };	
    }

    可以发现经过对高阶组件再进行了一层抽象后,前面的 withAdminAuth 可以写成 withAuth('Admin')了,如果此时需要 VIP 权限的话,只需在 withAuth 函数中传入 'VIP' 就可以了。

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

    组件渲染性能追踪

    借助父组件子组件生命周期规则捕获子组件的生命周期,可以方便的对某个组件的渲染时间进行记录:

    class Home extends React.Component {	
        render() {	
            return (&lt;h1&gt;Hello World.&lt;/h1&gt;);	
        }	
    }	
    function withTiming(WrappedComponent) {	
        return class extends WrappedComponent {	
            constructor(props) {	
                super(props);	
                this.start = 0;	
                this.end = 0;	
            }	
            componentWillMount() {	
                super.componentWillMount &amp;&amp; super.componentWillMount();	
                this.start = Date.now();	
            }	
            componentDidMount() {	
                super.componentDidMount &amp;&amp; super.componentDidMount();	
                this.end = Date.now();	
                console.log(`${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`);	
            }	
            render() {	
                return super.render();	
            }	
        };	
    }	
    export default withTiming(Home);

    640?wx_fmt=jpeg

    withTiming 是利用 反向继承 实现的一个高阶组件,功能是计算被包裹组件(这里是 Home 组件)的渲染时间。

    页面复用

    假设我们有两个页面 pageApageB 分别渲染两个分类的电影列表,普通写法可能是这样:

    // pages/page-a.js	
    class PageA extends React.Component {	
        state = {	
            movies: [],	
        }	
        // ...	
        async componentWillMount() {	
            const movies = await fetchMoviesByType('science-fiction');	
            this.setState({	
                movies,	
            });	
        }	
        render() {	
            return &lt;MovieList movies={this.state.movies} /&gt;	
        }	
    }	
    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 &lt;MovieList movies={this.state.movies} /&gt;	
        }	
    }	
    export default PageB;

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

    const withFetching = fetching =&gt; WrappedComponent =&gt; {	
        return class extends React.Component {	
            state = {	
                data: [],	
            }	
            async componentWillMount() {	
                const data = await fetching();	
                this.setState({	
                    data,	
                });	
            }	
            render() {	
                return &lt;WrappedComponent data={this.state.data} {...this.props} /&gt;;	
            }	
        }	
    }	
    // 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)) 抽离到外部传入,从而实现页面的复用。

    装饰者模式?高阶组件?AOP?

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

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

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

    使用装饰者模式实现 AOP

    面向切面编程(AOP)和面向对象编程(OOP)一样,只是一种编程范式,并没有规定说要用什么方式去实现 AOP。

    // 在需要执行的函数之前执行某个新添加的功能函数	
    Function.prototype.before = function(before = () =&gt; {}) {	
        return () =&gt; {	
            before.apply(this, arguments);	
            return this.apply(this, arguments);	
        };	
    }	
    // 在需要执行的函数之后执行某个新添加的功能函数	
    Function.prototype.after = function(after = () =&gt; {}) {	
        return () =&gt; {	
            const result = after.apply(this, arguments);	
            this.apply(this, arguments);	
            return result;	
        };	
    }

    可以发现其实 beforeafter 就是一个 高阶函数,和高阶组件非常类似。

    面向切面编程(AOP)主要应用在 与核心业务无关但又在多个模块使用的功能比如权限控制、日志记录、数据校验、异常处理、统计上报等等领域

    类比一下 AOP 你应该就知道高阶组件通常是处理哪一类型的问题了吧。

    总结

    React 中的 高阶组件 其实是一个非常简单的概念,但又非常实用。在实际的业务场景中合理的使用高阶组件,可以提高代码的复用性和灵活性

    最后的最后,再对高阶组件进行一个小小的总结:

    • 高阶组件 不是组件 一个把某个组件转换成另一个组件的 函数

    • 高阶组件的主要作用是 代码复用

    • 高阶组件是 装饰器模式在 React 中的实现

     热 文 推 荐 

    ☞ 浏览器中的垃圾回收与内存泄漏

    ☞ Web 组件势必取代前端?

    ☞ 作为一个开发者,我创业了

    ☞ 如何优雅处理前端的异常?

    ☞ TypeScript - 一种思维方式

    640?wx_fmt=png

    640?wx_fmt=png

    你也“在看”吗?

    展开全文
  • render-props和高阶组件

    2021-09-06 17:21:27
    render-props和高阶组件 以上两种方式都是实现状态组件复用 1.1 render props是一种模式 使用render创建复用组件 首先我们创建一个组件,这个组件里面有可以复用的方法和逻辑,下面我们写一个可以获取当前移动...

    render-props和高阶组件

    以上两种方式都是实现状态组件复用

    1.1 render props是一种模式

    使用render创建复用组件

    首先我们创建一个组件,这个组件里面有可以复用的方法和逻辑,下面我们写一个可以获取当前移动坐标的组件(mouse)

    import React, { Component } from 'react'
    export default class home extends Component {
      constructor(props) {
        super(props)
        this.state = {  
          x: 0,
          y: 0
        }
      }
      handleMouseMove = e => {
        console.log(e);
        this.setState({
          x: e.clientX,
          y: e.clientY
        })
      }
      render() {
        return this.props.render(this.state)   //将要复用的状态作为props.render(state)方法的参数,暴露到外部
      }
      componentDidMount() {    //在组件挂载完成进行鼠标移动事件监听  执行handleMouseMove方法
        window.addEventListener('mousemove', this.handleMouseMove)
      }
    }
    

    然后在需要该逻辑的组件中使用该组件

    import React, { Component } from 'react'
    import Home from './home'
    export default class asign extends Component {
     render() {
        return (
          <div>
           //传入参数mouse相当于上面暴露的this.state,在上面中组件暴露什么,那么这里接到的mouse就是什么
          //另外这里要注意这里一定要有返回值  因为返回的是数据状态 那么就要渲染HTML,或者null,不然就会报错
           <Home render={mouse=>{return <p>当前得位置{mouse.x}y轴{mouse.y}</p>}} />
          </div>
        )
      }
    }
    
    

    1.2 使用children创建复用组件

    注意此刻使用的是children就不是返回render 而是返回 this.props.children
    复用组件的render代码

     render() {
        return this.props.children(this.state)   //将要复用的状态作为props.render(state)方法的参数,暴露到外部
      }
    

    父组件使用复用组件的代码

      <Home>
         {  mouse => {
                return (
                  <p>{mouse}</p>
                )
            }
          }
      </Home>
    

    使用render props模式注意要添加props校验

    Mouse.propTypes={
    	children:PropTypes.func.isRequired
    }
    

    如果在使用复用组件没有传递参数就会报错 因为我写的校验规则是children的属性是必填项

    最后在做完所有操作之后 要特别注意一点,在react中所有使用window的方法,比如计时器,上面代码提到的鼠标监听事件,这些都是手动添加的,所以我们在组件销毁的时候一定要解绑事件,做到代码优化

      componentWillUnmount(){
        window.removeEventListener('mousemove',this.handleMouseMove)
      }
    

    高阶组件

    其实就是一个函数组件,接收要包装的组件,返回增强后的组件

    • 1.1 首先第一步创建一个函数,名称约定以with开头的js
      通俗来说高阶函数就好比手机套
    import React, { Component } from 'react'
    export default function withTest(WrappedComponent) {  //创建一个高阶函数以with开头,参数也必须是大写字母开头,因为我们的参数后面是当参数组件渲染的也就是,组件名由参数决定
      class test extends Component {   //创建一个类组件这里面写逻辑与状态,此组件相当于手机套的核心
        state = {
          x: 0,
          y: 0
        }
        hanldMouseMove = e => {
          this.setState({
            x:e.clientX,
            y:e.clientY
          })
        }
        //控制鼠标状态的逻辑
        componentDidMount() {
          window.addEventListener('mousemove', this.hanldMouseMove)
        }
        //自己创建的事件手动销毁
        componentWillUnmount(){
          window.removeEventListener('mousemove', this.hanldMouseMove)
        }
        render() {
        //返回一个组件,这个组件就是传进来的参数是什么名字就是什么组件,比如我在外部传入test那么最终生成的组件名就是test,相当于<test {...this.state} />
        //...this.state,就是传入状态
          return <WrappedComponent {...this.state}></WrappedComponent>
        }
      }
      return test //最终将这个方法返回
    }
    
    
    • 第二步 引入js并传入参数
    import React, { Component } from 'react'
    import withTest from './test'  //1.引入高阶函数
    import PropTypes from 'prop-types'
    const Position=props=>{
    	return (   //3.定义position的基本属性也就是传入参数的基本属性,props就是高阶函数传入的参数,在这里我们是解构state
      <p>
        横坐标{props.x}
        纵坐标{props.y}
      </p>
    )}
    const TestPosition = withTest(Position)//2.传入参数(Position),并且定义组件名(TestPosition )
    export default class asign extends Component {
      render() {
        return (
          <div className="home">
        	<TestPosition></TestPosition> //4.使用组件
          </div>
        )
      }
    }
    

    注意这样创建组件就有一个问题,那就是组件名相同,也就是手机壳的名字相同
    在这里插入图片描述

    为了区别组件那么就设置displayName便于区别调试时不同的组件,在react他的每个组件的命名都是通过displayName来命名的

    • 1.首先在高阶函数中添加一个名字方法
    //修改名字的方法
    function getDisplayName(WrappedComponent){
      console.log(WrappedComponent);
      return WrappedComponent.displayName ||WrappedComponent.name||'Component'
    }
    

    打印出WrappedComponent是这个结果,使用json转换也没用,我就不过多研究,有这方面的了解可以在评论中说出来,指正,我的理解是displayName 是react的组件内置的属性,包括nameComponent
    在这里插入图片描述

    • 然后在组件的返回值之前调用就行
     test.displayName=`WithTest${getDisplayName(WrappedComponent)}`
    

    完整代码
    在这里插入图片描述

    • 最终生成的组件名
      在这里插入图片描述

    3.高阶组件不能继续向下传递props,所以需要在高阶组件中把props一起传递给组件

    在不传递props会发现在组件中是无法获取到的
    在这里插入图片描述

    在这里插入图片描述
    但是高阶函数那边可以打印 ,说明高阶函数那边接收到了但是没有往下传,解决办法只需要将props往下传就行

    在这里插入图片描述
    在这里插入图片描述
    这里就获取到了

    展开全文
  • 高阶组件理解 本章节,我们将深入高阶组件详细了解,高阶组件相关的知识点,包括什么是高阶组件,以及如何创建高阶组件高阶组件实现的几种方式,常用的使用场景等等; ...
  • 一、是什么高阶函数(Higher-order function),至少满足下列一个条件的函数接受一个或多个函数作为输入输出一个函数在React中,高阶组件即接受一个或多个组件作为参数并且返...
  • 早期利用 ES5 的 mixin 语法来做的事,基本都可以使用高阶组件代替,而且能做的还有更多。 前言 写这篇文章的起因是其他关于高阶组件(Higher-Order Components)的文章,包含官方文档,都令初学者感到相当困惑。我...
  • 1.为何使用高阶组件 实现逻辑复用,例如:用户对页面按钮操作的权限控制。 2.什么是高阶组件 React官网:具体而言,高阶组件是参数为组件,返回值为新组件的函数。 3.应用场景 鉴权判断 举个栗子: 下面代码是三块...
  • 高阶组件就是接受一个组件作为参数并返回一个新组件(功能增强的组件)的函数。这里需要注意高阶组件是一个函数,并不是组件,这一点一定要注意。 使用场景 将几个功能相似的组件里面的方法和react特性(如生命周期...
  • 组件和高阶组件区别The maintainable component structure is a crucial prerequisite for a stable React application. You can achieve this by writing your code in a functional way using higher-order ...
  • 返回的新组件直接继承自React.Component类,新组件扮演的角色传入参数组件的一个代理,在新组件的 render 函数中,将被包裹组件渲染出来,除了高阶组件自己要做的工作,其余功能全都转手给了被包裹的组件 应用场景 应用 1...
  • React高阶组件实践

    2019-09-09 11:47:02
    React高阶组件,即Higher-Order Component,其官方解释是: A higher-order component is a function that takes a component and returns a new component. 一个传入一个组件,返回另一个组件的函数,其概念与高...
  • 简单理解高阶组件

    2021-03-24 10:17:33
    什么是高阶组件高阶组件是包装了另一个React组件的React组件 怎么理解高阶组件 ​ 可以简单将其理解成 “类工厂” ​ 将一些可复用的逻辑写在高阶组件中,把一个普通组件传入高阶组件,通过返回值的形式,将...
  • 1.什么是高阶组件?...我们往往把高阶组件定义为一个函数组件,因为通过使用函数我们可以很方便接受一个组件作为原始组件,然后返回一个新的组件,在这个新组件中挂载渲染原始组件,同时传入所有的pr...
  • 高阶组件:本质是一个函数,返回值是一个组件,函数的参数是原生组件,主要是对组件的代码的复用或者是逻辑上的复用, 以及对原始组件上的props和state进行增删改查的操作,也可以对原生组件的进行增强和优化; 高阶...
  • 由于它是一个高阶组件,导致父组件去获取实例的时候一直都拿不到内部的值和方法。下面就是大概代码。 使用了hooks,点击看这里 子组件代码 import { Form } from 'antd' @Form.create() export default class index ...
  • React总结篇之六_React高阶组件

    千次阅读 2018-11-20 11:23:43
    高阶组件(Higher Order Component,HOC)并不是React提供的某种API,而是使用React的一种模式,用于增强现有组件的功能。一个高阶组件就是一个函数,这个函数接受一个组件作为输入,然后返回一个新的组件作...
  • 所以,我建议你可以少花点时间去堆业务代码,多去关心一下框架内部的实现原理,通过源码的剖析看到框架的本质,这样才有助于你走的更、更远。 今天,分享给你一份耗时 6个月精心整理,最适合前端人进阶的超全资料...
  • 背景介绍: 随着项目越来越大,性能问题已经成为了困扰业务发展的重要因素。 功能不停地累加后,核心页面已经不堪重负,访问速度愈来愈慢。...突然了解到vue的异步组件,于是便专门研究实践了一下。 ...
  • 浅谈React高阶组件

    2020-12-12 00:17:31
    但经过了种种衡量,最后选择使用高阶组件的做法。 那什么是高级组件?首先你得先了解请求ES6中的class只是语法糖,本质还是原型继承。能够更好的进行说明,我们将不会修改组件的代码。而是通过提供一些能够包裹...
  • 继承方式的高阶组件

    2021-06-30 16:24:34
    继承方式的高阶组件采用继承关系关联作为参数的组件和返回的组件 ...继承方式的高阶组件可以应用于下列场景 操纵prop 除了上面不安全的直接修改this.props方法,还可以利用React.cloneElement让组件从新绘制 唯一用得.
  • 详解react hooks(含高阶组件)

    万次阅读 多人点赞 2020-06-01 15:22:38
    取代高阶组件和render props来实现抽象和可重用性 在hooks出现之前,只有在类组件中可以使用本地状态管理和生命周期方法,函数组件只能是无状态组件,因为函数组件使用便利优雅,已经被广泛使用,后期如果函数组件...
  • 高阶组件(Higher Order Component, HOC)并不是React提供的某种API,而是使用React的一种模式,用于增强现有组件的功能。 简单来说,一个高阶组件就是一个函数,这个函数接受一个组件作为输入,然后返回一个新的...
  • 组件参数的编译期检查随着TypeScript语言特性的完善,我们团队的前端代码已完全迁移至TypeScript。当前TypeScript完全支持JSX语法,举个例子:import * as React from 'react' type Props = { p1: string } class...
  • React教程之高阶组件

    2019-03-15 07:47:56
    通过这篇文章你可以学到高阶组件的定义及原理、高阶组件在项目中的常见应用、通用高阶组件如何封装以及继承方式高阶组件和代理方式高阶组件。 搭建项目 create-react-app myapp创建一个react项目,并在src目录下新建...
  • React中的高阶组件(HOC)

    千次阅读 2017-07-22 16:46:12
    简要介绍:React中不会用到组件的继承,作者选择用组合来代替继承,但是存在一种情况,就是两种组件方法类似,...1、什么是高阶组件(high order component)高阶组件的定义:就是一个函数,这个函数传入一个组件,然后
  • 深入理解React 高阶组件

    千次阅读 2017-07-15 09:47:54
    1. 基本概念高阶组件是React 中一个很重要且较复杂的概念,高阶组件在很多第三方库(如Redux)中都被经常使用,即使你开发的是普通的业务项目,用好高阶组件也能显著提高你的代码质量。高阶组件的定义是类比于高阶...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 10,294
精华内容 4,117
关键字:

高阶组件的使用场景