React中的代码复用-HOC

代码的组织形式

所有的前端框技术要面对一个问题:如何组织UI布局与页面逻辑

对于UI布局,基本上都提供xml布局手写两种方式。xml布局适合简单的场景,相对来说更加直观;手写则适合复杂的,逻辑较多的场景,相对来说更加灵活。

当使用xml做UI布局时:

  • 有些框架将UI和逻辑做物理隔离,通过某种形式关联二者,如Android原生、iOS原生等;
  • 有些框架则只做代码区块的隔离,在同一个文件的不同区块中,分别组织UI和逻辑,如Vue;

而React在JSX的规则下,UI、逻辑代码可以混写,既有xml的便捷性,又有手写的灵活性。

而代码的组织形式,直接影响开发的开发习惯、开发中的共性问题、封装代码的模式、流行的三方库等。

前端开发面临的问题

具体某种前端技术的组织形式,是由框架的官方设计者来决定的。但是基于这种组织形式,在实际开发中所遇到的主要问题,往往是随着技术的推广以及应用的复杂逐渐放大暴露出来的。如原生开发的超级Controller,React中的超级组件等。

这些问题看似与具体的技术平台有关,但抽象地来看,前端开发者面临两个共性问题:

  • 「如何避免单个页面文件过大」
  • 「将共同点优雅的抽取出来,在不同的地方复用」

一般来说,官方会给出一些设计模式的建议,高级开发者会先研究学习,之后通过内部分享、博客、招聘等形式推广到广大初中级开发者,形成模式的流行,如Android的MVP,React中的HOC等。虽然问题是共性的,但解决问题的方式是与语言特性以及代码的组织形式相关的,每种技术有各自流行模式 。

React中的复用场景

简单的场景

当组件过大时,最简单的方式是拆分组件和工具函数。这是最基础的、应用最广泛的两种代码复用方式。

拆分组件可解决文件过大的问题,也可以通过在多个地方复用。而工具函数是纯函数,比较适合封装纯逻辑,如格式化、公式计算等,无法封装带状态的逻辑。用好这两种方式,可以支持一般的业务功能的开发维护。

复杂的场景

实际的项目中,往往有着更复杂的封装需求,如在多个组件中进行一些统一的行为:

  • 增加页面的切入和切出日志(封装逻辑)
  • 在页面加载时获取及处理数据,并在页面卸载时销毁相关的监听、定时器等(封装状态与逻辑)
  • 在props中增加router对象(props增强)
  • 为页面增加生命周期函数(组件增强)
  • 在列表数据为空的时候,显示为无数据的状态(渲染劫持)

如何复用这些封装,往往是库开发者、项目主程、以及复杂的业务模块负责人要面临的问题。

高阶组件(HOC)

组件可以看成是一个输入为props,输出为「界面及交互」的纯函数,高阶组件则可以看成一个输入为组件,输出为增强后的组件的一个纯函数

高阶组件有两种实现方式:代理式与继承式。官方推荐的代理式,相对来说使用场景更广泛。但在某些场景下,继承式更加灵活方便。

代理式HOC的例子

最常见的就是props增强:

scala 复制代码
export const InjectRouter = (ComponsedComponent) => {
  return class extends React.Component {
    ...

    render() {
        return (
          <ComponsedComponent {...props} router={RouterManager.getRouter()} />
        );
      }
    }
  }
}

我们可以方便的在代码中调用this.props.router.push,而无需显式的import相关的router对象。

渲染拦截:

scala 复制代码
export const withLoading = (ComponsedComponent) => {
  return class extends React.Component {
    constructor() {
      super();
      this.state = {
        loading: false
      }
    }
    render() {
        return (
          this.state.loading ? 
          <Loading/ > :
          <ComponsedComponent {...props} setLoading={value => this.setState(value) }/>
        );
      }
    }
  }
}

在组件中,可以通过调用this.props.setLoading()来控制loading显示。

封装状态逻辑:

kotlin 复制代码
// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
  // ...并返回另一个组件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ...负责订阅相关的操作...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... 并使用新数据渲染被包装的组件!
      // 请注意,我们可能还会传递其他属性
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

基本上所有对数据逻辑的封装,都可以通过这种形式。

继承式HOC的例子

继承式最典型的场景就是生命周期增强,可以直接访问到父组件的内部的生命周期函数:

scala 复制代码
// 继承式生命周期增强HOC
const withMoreLifecycle = WrappedComponent => {
  return class C extends WrappedComponent {
    componentDidMount() {
      super.componentDidMount && super.componentDidMount()
      setTimeout(() => {
        super.componentDidMount2Seconds && super.componentDidMount2Seconds();
      }, 2000)
    }

    render() {
      const element = super.render();
      return element;
    }
  }
}

继承式也可以做拦截渲染,比代理式更强大,可以操作到组件内部:

javascript 复制代码
class Index extends React.Component{
  render(){
    return <div>
       <ul>
         <li>react</li>
         <li>vue</li>
         <li>Angular</li>
       </ul>
    </div>
  }
}

function HOC (Component){
  return class Advance extends Component {
    render() {
      const element = super.render()
      const otherProps = {
        name:'alien'
      }
      /* 替换 Angular 元素节点 */
      const appendElement = React.createElement('li' ,{} , `hello ,world , my name  is ${ otherProps.name }` )
      const newchild =  React.Children.map(element.props.children.props.children,(child,index)=>{
           if(index === 2) return appendElement
           return  child
      }) 
      return  React.cloneElement(element, element.props, newchild)
    }
  }
}
export  default HOC(Index)

继承式HOC的主要问题在于,在HOC嵌套的情况下,不允许在它的前面有其他代理式的HOC,因为代理式HOC会拦截所有的super。 另外一个问题是,继承式不允许使用函数组件,只能使用类组件。

代理与继承的对比

从上面的例子可以看出,代理式更多的是在一个组件的外围做工作,如增强props、条件渲染等。而继承式似乎更容易深入到一个组件的内部去,如调用组件的生命周期函数、访问组件的props、state等。

但两种没有绝对的界限,大部分封装,两种都可以做,用哪种不用哪种,往往会基于两个因素来考量:

  • 方不方便,很多时候两种都可以实现,我们倾向于更方便的那个,如props增强更倾向于使用代理式,而生命周期增强则倾向于使用继承式。

从下面这个表可以看出在不同的场景下,哪种更方便:

场景 更方便的方式
props增强 代理式
封装状态逻辑 代理式
生命周期增强 继承式
渲染劫持 根据情况而定
  • 有什么后续的影响,继承式HOC往往对组件有更多的侵入性,对组件有一定假设基础,适用于内部项目的某一类组件。如果使用了继承式的HOC,则要保证团队内达成共识,即"在继承式HOC其前面,不会有代理式HOC"。

他们的差异可以从下面这个表来对比:

特性 代理式 继承式 备注
侵入性
适用范围 类组件与函数组件 类组件
静态属性 无法继承 可以继承

总结

HOC是React16.8之前组件间复用代码的主要方式,从场景上来说可用于props增强、封装状态逻辑、生命周期增强、渲染劫持等场景;从形式上来说可分为代理式与继承式HOC,这两种各有各的优缺点,需要根据项目类型和场景来决定。

React从16.8开始,提供了一种全新的方式-hook来复用代码

相关推荐
Senar6 小时前
如何判断浏览器是否开启硬件加速
前端·javascript·数据可视化
HtwHUAT6 小时前
实验四 Java图形界面与事件处理
开发语言·前端·python
利刃之灵6 小时前
01-初识前端
前端
codingandsleeping6 小时前
一个简易版无缝轮播图的实现思路
前端·javascript·css
天天扭码6 小时前
一分钟解决 | 高频面试算法题——最大子数组之和
前端·算法·面试
全宝7 小时前
🌏【cesium系列】01.vue3+vite集成Cesium
前端·gis·cesium
拉不动的猪7 小时前
简单回顾下插槽透传
前端·javascript·面试
烛阴7 小时前
Fragment Shader--一行代码让屏幕瞬间变黄
前端·webgl
爱吃鱼的锅包肉7 小时前
Flutter路由模块化管理方案
前端·javascript·flutter
风清扬雨8 小时前
Vue3具名插槽用法全解——从零到一的详细指南
前端·javascript·vue.js