React组件中的逻辑复用

1,246 阅读10分钟

React组件中的逻辑复用

React中的逻辑复用主要涉及三个方面, 渲染属性render prop、高阶组件HOC、自定义Hook, 这三个是我们实际项目工作中最常用到的, 下面我们来详细的捋一捋他们各自的使用场景,及异同点。

render props

渲染属性React中逻辑复用的一种方式,可以复用组件内部的状态、逻辑, 通过提供一个函数类型的prop来决定组件内部如何渲染,同时达到复用该组件内部状态、逻辑的能力;

 //鼠标跟踪功能, 时刻显示鼠标当前所在的位置坐标
 class MouseTracker extends React.Component{
   constructor(props) {
     super(props)
     this.state = {x:0, y:0}
   }
   //鼠标移动事件
   handleMouseMove(event) {
     this.setState({
       x: event.clientX,
       y: event.clientY
     });
   }
   
   render() {
     <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
       <h1>移动鼠标!</h1>
       <p>当前的鼠标位置是 ({this.state.x}, {this.state.y})</p>
     </div>
   }
 }

如果在别的组件中也需要鼠标跟踪的功能,我们如何才能复用鼠标跟踪的这个逻辑呢?这里困难的地方是,这里的鼠标状态和鼠标跟踪逻辑都在该组件内部, 那我们怎么才能在其他组件中使用这些东西呢?

官方文档里给出了一个很恰当例子: 我们想要一个显示小猫🐱的能时刻追踪鼠标的位置;明显 要实现这个功能就需要鼠标跟踪的逻辑, 那么就又回到上面提出的问题了,怎么才能复用MouseTracker组件中的状态跟逻辑呢?

 <Cat style={{ position: 'absolute', left: mouseInfo.x, top: mouseInfo.y }}></Cat>

貌似我们只要将Cat组件放在MouseTracker组件内部就能够获取后者的状态跟逻辑了,通常我们若想让一个组件在另一个组件的内部进行展示,有两种实现方式:一种是通过props.children,让Cat以子组件的形式传递进来:<MouseTracker> <Cat/> </MouseTracker>;第二种那就是直接改造原始的MouseTracker组件,直接把Cat组件的内容添加到MouseTracker组件中; 这样Cat组件是放进来了,但是这两种方式都不能满足我们的需求:

第一种方式: 虽然通过props.children,现在Cat组件能够在MouseTracker组件内展示了, 但是如何将MouseTracker组件内中鼠标的位置信息mouseInfo设置到<Cat/>组件身上呢?但是按照下面的这种写法、没有设置的机会啊。

 //1. 将Cat组件放在MouseTracker组件内
 <MouseTracker> <Cat/> </MouseTracker>
 ​
 //2. 在MouseTracker组件内部通过props.children获取
 class MouseTracker extends React.Component{
  // state信息......
   render() {
     <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
       { this.props.children }
     </div>
   }
 }

第二种方式:通过改变原始MouseTracker组件倒是能够设置鼠标信息,但是这种改变原始组件的做法,没有实现组件复用的效果,只能用于特定用例中, 下次再用到时,还是再需要封装一个新的组件。

 class MouseTracker extends React.Component{
   //  state信息......
   render() {
     <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
       <Cat style={{left: this.state.x, top: this.state.y }}></Cat>
     </div>
   }
 }

回到上面提到的渲染属性render prop,在父组件中以函数的形式传入要渲染的内容, 同时还能复用MouseTracker组件内的状态与逻辑, 实现代码复用的功能;

 class MouseTracker extends React.Component{
   //  state信息......
   render() {
     <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
       { this.props.render(this.state) }
     </div>
   }
 }
 //通过render prop的形式传入要渲染的内容, 同时将鼠标信息以参数的形式传入
 <MouseTracker render={ (mouseInfo) =>  <Cat style={{ left: mouseInfo.x, top: mouseInfo.y }}></Cat>} />
 ​

render prop是因为模式才被称为render prop, 你不一定要用名为renderprop来使用这种模式。事实上,任何被用于告知组件需要渲染什么内容的函数prop在技术上都可以被称为render prop.

其实上面说到的props.children值的类型也可以是函数, 可以像渲染属性render prop那样,通过一个函数的形式,接收状态信息同时返回要渲染的组件内容, 此时也能实现复用组件内部状态、逻辑的功能:

 class MouseTracker extends React.Component{
   //  state信息......
   render() {
     <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
       //由于子组件children是一个函数组件, 所以可以通过参数的形式,将状态传入子组件内部
      { this.props.children(state) }
     </div>
   }
 }
 ​
 //children以函数的形式给出
 <MouseTracker render={ (mouseInfo) =>  <Cat style={{ left: mouseInfo.x, top: mouseInfo.y }}></Cat>}>
   {
     state => <Cat style={{ left: state.x, top: state.y }}></Cat>
   }
 </MouseTracker>

高阶组件

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

具体而言,高阶组件是一个函数,该函数接收一个组件作为参数,并返回另外一个新的组件:

 function higherOrderComponent (WrappedComponent) {
   return class extends React.Component {
     constructor(props) {
       super(props)
       this.state = { /**other Data*/}
     }
     render() {
       return <WrappedComponent {...this.props} data={...this.state}/>
     }
   }
 }
 const EnhancedComponent = higherOrderComponent(WrappedComponent);

HOC的两种实现方式

属性代理

所谓属性代理就是在需要增强的业务组件外层,再包裹一层代理组件, 在代理组件上我们可以做一些对原业务组件的代理操作; 在生命周期体现上就是,先调用业务组件的componentDidMount再调用外层代理组件的componentDidMount,所以外层的代理组件跟被包裹的业务组件是父子组件的关系;在代码成面上的体现就是, HOC函数返回一个容器组件(可以是有状态的类组件、也可以是无状态的函数组件),在返回的容器组件中包含上需要增强的业务组件。HOC不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC通过将组件包装在容器组件中来组合成新的组件。HOC是纯函数,没有副作用。被包装组件接收来自容器组件的所有prop,同时也可以接收一个新的用于renderdata propHOC不需要关心数据的使用方式或原因, 而被包装组件也不需要关心数据是怎么来的。这种方式生成的高阶组件用如下:

  1. 功能复用

直接整体复用业务组件功能,这是高阶组件最根本的功能;但是不要去试图修改被包装的组件,因为这会让组件后期功能变得不可预测:

 class WrappedComponent extends React.Component {
     shouldComponentUpdate() { 
       return true 
     }
     render() {
       return <div>WrappedComponent</div>
     }
  }
 ​
 //试图在HOC中,去修改被包装组件的功能
 function HOC1 (WrappedComponent) {
   return class extends React.Component {
     constructor(props) {
       super(props)
       this.state = { /**other Data*/}
     }
     //改变原组件的方法
      WrappedComponent.prototype.shouldComponentUpdate = function() {
         return false
       }
     render() {
       return <WrappedComponent {...this.props} data={...this.state}/>
     }
   }
 }     

在高阶组件的使用过程中,这种在组件外部通过原型的方式改变了原组件自身的逻辑;当下次需要再次引用该组件时, 此时引用的却是被更改后的组件,可我们却还不知道,这个逻辑在什么地方被更改的(因为不是在当前这个组件里改的)。

  1. props属性劫持

可以操作外部传入的props(不包括refkey, 这两个属性比较特别,会被单独处理不会向下传递, 如果将refkey添加到HOC返回的组件上,那么refkey的引用指向外层的容器组件,而不是被包装组件),在props传入到被包装组件之前可以对其进行增、删、改的操作:

 function higherOrderComponent (WrappedComponent, otherData) {
   return function(props) {
      const newProps = {
         ...props,
        ...otherData,
        name: 'update value',
       }
     return <WrappedComponent {...newProps} />
   }
 }
  1. 渲染劫持

    可以根据外部传入的属性值,来决定组件如何显示(处理加载中、无数据...何时显示):

 // 条件渲染
 function higherOrderComponent (WrappedComponent) {
   return class extends React.Component {
     render() {
       if( /**渲染条件*/ ) { return null}
       else if( /**渲染条件*/) { return 'loading' }
       else return <WrappedComponent {...this.props}/>
     }
   }
 }
 // 组合渲染
 function higherOrderComponent (WrappedComponent) {
   return class extends React.Component {
     render() {
       return (
         <div>
            {/* 组合渲染内容 */}
           <div>{this.props.title}</div>
           <WrappedComponent {...this.props}/>
         </div>
       )
     }
   }
 }
  1. 功能增强

这其实也算是一种对props的操作; 如果我们想要让一些组件,都具备某一个功能该怎么办呢?用过Redux库的我们应该都知道,组件在经过Connect之后,在组件内部都会有一个dispatch方法, 我们在使用Redux的时候有没有想过这个dispatch是从哪来的呢?其实这个dispatch就是通过高阶组件强化时,从外部通过props传入的。

属性代理方式实现的高阶组件也一些不足之处或者说是办不到的地方:

  1. 无法直接获取业务组件的内部状态State

    通常在组件内部我们通过this.state直接获取状态; 如果想要在组件外部获取该组件内部的state时,那就只能通过ref了, 所以在高阶组件中想要获取被包裹组件的状态,就只能在被包裹组件上设置ref来间接获取了。

  2. 无法直接调用业务组件的静态方法

    ES6中类提供的静态属性或方法,只能被类自己调用, 不能被类的实例对象调用, 当业务组件经HOC强化后,返回的是另外一个新的组件,新返回的这个组件上当然是没有被包装组件的静态属性的;

     //业务组件
     class WrappedComponent extends React.Component {
         static staticMethod() { /* staticMethod */ }
         render() {
           return <div>WrappedComponent</div>
         }
      }
     ​
     //经HOC增强后
     const EnhancedComponent = HOC(WrappedComponent);
     EnhancedComponent.staticMethod() // EnhancedComponent.staticMethod is not a function
     ​
    

    如果我们想要在外层容器组件中使用内层业务组件提供的静态方法,就只能手动将这些静态方法拷贝到外层的容器组件上了:

     function HOC (WrappedComponent) {
       class EhancedComponent extends React.Component {   
         render() {
           return <WrappedComponent { ...this.props }/>
         }
       }
       //手动复制内层业务组件的静态方法到外层容器组件
       EhancedComponent.staticMethod = WrappedComponent.staticMethod;
       return EhancedComponent
     }  
    
2. 反向继承

反向继承也是通过一个函数,接收一个组件作为参数,并返回另外一个新的组件。与属性代理的区别是:返回的这个新组件需要继承接收的这个参数组件,同时需要在返回的新组件的render方法中调用参数组件的render()

 function HOC (WrappedComponent) {
   class EhancedComponent extends WrappedComponent {   //继承被包裹的组件
     render() {
       return super.render() //通过super调用父类中的render方法
    }
 }

由于EhancedComponentWrappedComponent之前是继承关系, 所以在子类EhancedComponent中可以直接获取到父类WrappedComponent中的属性、方法(state、props、key、ref、静态方法、生命周期);但是想直接通过this获取到这些属性、方法的前提是:子类中没有同名的属性、方法, 否则获取到的就是子类自己的属性、方法;

通过属性代理方式实现的高阶组件,所具备的功能(功能复用、props属性劫持、渲染劫持、功能增强),反向继承都具备;而且前者不具备的功能(无法直接获取被包裹组件的内部状态、无法直接调用被包裹组件的静态方法、无法操作被包裹组件的生命周期。。。),后者也都具备, 但是反向继承只能用于增强类组件,对函数组件不适用;

从使用上来说,属性代理方式更像是从WrappedComponent外部入手,通过props来实现想要的功能,而反向继承则更像是直接从WrappedComponent内部入手, 通过操作继承的属性来实现想要的功能。

自定义hook

v16.8之后,React引入Hook。通过自定义Hook,可以实现组件逻辑复用。通常当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。与普通函数的区别是自定义Hook必须以use开头命名;同时自定义Hook在内部可以调用其他Hook,可以根据需要接受任意参数、或者返回任何我们想要的类型值;

 import { useState, useEffect } from 'react';
 ​
 //根据好友ID返回好友是否在线
 function useFriendStatus(friendID) {
   const [isOnline, setIsOnline] = useState(null);
 ​
   useEffect(() => {
     function handleStatusChange(status) {
       setIsOnline(status.isOnline);
     }
 ​
     ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
     return () => {
       ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
     };
   });
 ​
   return isOnline;
 }

既然自定义hook的最主要功能就是实现逻辑复用,减少相同逻辑代码的书写。 这句话本身没有毛病, 但是这个功能,用一个普通的函数不就能够做到吗?把组件中的相同逻辑代码提取到一个函数中,然后在需要的地方直接调用这个函数不就行了, 那为啥React还要再引入一个自定义hook 呢?因为普通函数无法做到模仿生命周期的功能(componentDidMountcomponentDidUpdatecomponentUnMount