高阶组件

228 阅读3分钟

是什么?

  • 一个基于React组件概念的函数:接收一个组件为参数,并返回一个新的组件函数
  • 一个复用组件逻辑的高级技巧

为什么?

  • 可以增强代码的复用性和灵活性,提高开发效率

  • 对高阶组件的理解有助于我们理解各种React第三方库

  • 解决以下三个问题

    • 抽取重复代码,实现组件复用,常见场景:页面复用
    • 条件渲染,控制组件的渲染逻辑(渲染劫持),常见场景:权限控制
    • 捕获/拦截生命周期,常见场景:组件渲染性能追踪日志打点

实现

属性代理

可以实现对接收的组件进行一些操作:

修改props
function HOC(WrapperComponent) {
    return class extends React.component {
        render() {
            const newProps = { name: '333' }
            <WrapperComponent {...this.props} { ...newProps }/>
        }
    }
}

像上面这样,组件永远都能接受到一个name属性的props

抽象state
function HOC(WrapperComponent) {
    return class extends React.Component {
        constructor(props) {
            super(props)
            this.state = {
                count: 0
            }
        }
        handleCountChange = () => {
            this.setState(prevState => {
                return {
                    count: prevState.count + 1,
                }
            })
        }
        render() {
            const newProps = {
                count: this.state.count,
                handleCountChange: this.handleCountChange.bind(this);
            }
            return (
                <WrapperComponent {...props} {...newProps} />
            )
        }
    }
}

上面的例子,就把接收到的所有WrapperComponent组件的count和handleCountChange给抽象出来了

操作refs
function HOC(WrapperComponent) {
    return class extends React.Component {
        componentRef = React.createRef();
        componentDidMount() {
            // 这里就拿到了WrapperComponent的实例方法
            this.componentRef.current.focus();
        }
    
        render() {
            return (<WrapperComponent ref={this.componentRef} {...this.props}/>)
        }
    }
}
通过props控制渲染条件
function HOC(WrapperComponent) {
    return class extends React.Component {
        render() {
            if (this.props.hasRight) {
                return <WrapperComponent {...this.props}/>
            }
            return <div>没有权限</div>
        }
    }
}
对接收的组件进行包裹
function HOC(WrapperComponent) {
    return class extends React.Component {
        render() {
            return (
                <div>
                    // 做花里胡哨的包装
                    <WrapperComponent/>
                </div>
            )
        }
    }
}

反向继承

最简单,且毫无意义的例子

function HOC(WrapperComponent){
    return class extends WrapperComponent {
        render() {
            return super.render();
        }
    }
}
劫持生命周期和方法
function HOC(WrapperComponent){
    return class extends WrapperComponent {
        // 这里会重写这个方法
        componentDidMount() {
            // do something...
        }
        render() {
            return super.render();
        }
    }
}
读取/操作state
function HOC(WrapperComponent) {
    return class extends WrapperComponent {
        componentDidMount() {
            console.info(this.state) // 获取state
            this.setState({ name: 12 }) // 修改
        }
    }
}

实践

页面复用

这个也是最常见的例子,如果两个页面,有很多共同的方法、数据

// pageA
class PageA extends React.Component {
    state = {
        list: []
    }
    
    async componentDidMount() {
        const newList = await getData("hanguo")
        this.setState({ list: newList })
    }
    
    render() {
        return <List list={this.state.list}/>
    }
}


// pageB
class PageB extends React.Component {
    state = {
        list: []
    }
    
    async componentDidMount() {
        const newList = await getData("rimei")
        this.setState({ list: newList })
    }
    
    render() {
        return <List list={this.state.list}/>
    }
}

从上面例子可以看到,PageA和PageB很多地方都可以公用,唯一的区别就是getData接收的参数不同

function HOC(WrapperComponent, type) {
    return class extends React.Component {
        state = {
            list: []
        }

        async componentDidMount() {
            const newList = await getData(type)
            this.setState({ list: newList })
        }

        render() {
            return <WrapperComponent list={this.state.list}/>
        }
    }
}

// pageA.js
HOC(List, "hanguo")

// pageB.js
HOC(List, "rimei")

如此便设计了一个高阶组件,完成了组件的公共方法的提取和扩展

权限控制
import React from 'react';
import { whiteListAuth } from '../lib/utils'; // 鉴权方法

/**
 * 白名单权限校验
 * @param WrappedComponent
 * @returns {AuthWrappedComponent}
 * @constructor
 */
function AuthWrapper(WrappedComponent) {
  return class AuthWrappedComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        permissionDenied: -1,
      };
    }
    
    async componentDidMount() {
      try {
        await whiteListAuth(); // 请求鉴权接口
        this.setState({
          permissionDenied: 0,
        });
      } catch (err) {
        this.setState({
          permissionDenied: 1,
        });
      }
    }
    
    render() {
      if (this.state.permissionDenied === -1) {
        return null; // 鉴权接口请求未完成
      }
      if (this.state.permissionDenied) {
        return <div>功能即将上线,敬请期待~</div>;
      }
      return <WrappedComponent {...this.props} />;
    }
  }
}

export default AuthWrapper;
组件渲染性能追踪

利用反向继承,在生命周期中加入数据,监控渲染完成时间

function RenderTime(WrapperComponent) {
    let start, end = 0;
    return class extends WrapperComponent {
        componentWillMount() {
            super.componentWillMount();
            start = Date.now();
        }
        
        componentDidMount() {
            super.componentDidMount();
            end = Date.now();
            console.info(本次渲染完成时间, end - start) // 伪代码
        }
        render() {
            return super.render();
        }
    }
}

总结

  • 高阶组件不是组件,它是一个将某个组件转换成另一个组件的纯函数。

  • 高阶组件的主要作用是实现代码复用和逻辑抽象、对 stateprops 进行抽象和操作、对组件进行细化(如添加生命周期)、实现渲染劫持等。在实际的业务场景中合理的使用高阶组件,可以提高开发效率和提升代码的可维护性。

  • 高阶组件的实用性 💪使其频繁地被大量 React.js 相关的第三方库,如 React-Reduxconnect 方法、React-Loadable等所使用,了解高阶组件对我们理解各种 React.js 第三方库的原理很有帮助 👍。

  • 高阶组件有两种实现方式,分别是属性代理和反向继承。它可以看作是装饰器模式在 React 中的实现:在不修改原组件的情况下实现组件功能的增强。