React 中的高阶组件及其应用场景

转自React 中的高阶组件及其应用场景

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

本文目录:git

  • 什么是高阶组件
  • React 中的高阶组件
    • 属性代理(Props Proxy)
    • 反向继承(Inheritance Inversion)
  • 高阶组件存在的问题
  • 高阶组件的约定
  • 高阶组件的应用场景
  • 装饰者模式?高阶组件?AOP?
  • 总结

什么是高阶组件

在解释什么是高阶组件以前,能够先了解一下什么是 高阶函数,由于它们的概念很是类似,下面是 高阶函数 的定义:github

若是一个函数 接受一个或多个函数做为参数或者返回一个函数 就可称之为 高阶函数编程

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

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

高阶组件 的定义和 高阶函数 很是类似:数组

若是一个函数 接受一个或多个组件做为参数而且返回一个组件 就可称之为 高阶组件app

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

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

因此你可能会发现,当高阶组件中返回的组件是 无状态组件(Stateless Component) 时,该高阶组件其实就是一个 高阶函数,由于 无状态组件 自己就是一个纯函数。async

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

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 组件包一层背景色为 #fafafadiv 元素:

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>
            );
        }
    };
}
复制代码复制代码

在这个例子中利用高阶函数中能够读取 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 <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 &lt;Enhance {...props} forwardRef={ref} /&gt;
}

return React.forwardRef(forwardRef);
复制代码
复制代码// 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 <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-reduxconnect 方法的使用方式很是像?没错,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

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 <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;
    };
}
复制代码复制代码

能够发现其实 beforeafter 就是一个 高阶函数,和高阶组件很是相似。

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

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

总结

React 中的 高阶组件 实际上是一个很是简单的概念,但又很是实用。在实际的业务场景中合理的使用高阶组件,能够提升代码的复用性和灵活性

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

  • 高阶组件 不是组件 一个把某个组件转换成另外一个组件的 函数
  • 高阶组件的主要做用是 代码复用
  • 高阶组件是 装饰器模式在 React 中的实现
相关文章
相关标签/搜索