首页   

react 高阶组件HOC详解

铸心  ·  · 2 年前
阅读 28

react 高阶组件HOC详解

参考链接:

juejin.cn/post/684490…

juejin.cn/post/684490…

前言

高阶组件与自定义hooks是React 目前流行的状态逻辑复用的两种解决方案

1.高阶组件是什么

高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。

高阶组件(HOC)是React中的高级技术,用来重用组件逻辑。但高阶组件本身并不是React API。它只是一种模式,这种模式是由React自身的组合性质必然产生的。

HOC简单例子:

//HOC
function visible(WrappedComponent) {
  return class extends Component {
    render() {
      const { visible, ...props } = this.props;
      if (visible === false) return null;
      return <WrappedComponent {...props} />;
    }
  }
}

// 用HOC包裹组件
class Example extends Component {
  render() {
    return <span>示例组件</span>;
  }
}
export default HOC(Example)
//或者用decorator方式
@HOC
class Example extends Component {
   render() {
    return <span>示例组件</span>;
  }
}
export default Example

//使用
<Example visible={false}/>

复制代码

上面的代码就是一个HOC的简单应用,函数接收一个组件作为参数,并返回一个新组件,新组建可以接收一个visible props,根据visible的值来判断是否渲染传入的组件。

2 高阶组件实现方式

2.1属性代理

将一个React组件作为参数传入函数中,函数返回一个自定义的组件。该自定义组件的render函数中返回传入的React组件。

由此可以代理并操作传入的React组件的props,并且决定如何渲染,实际上 ,这种方式生成的高阶组件就是原组件的父组件,上面的函数visible就是一个HOC属性代理的实现方式。

这种实现方式下,HOC容器组件和传入组件的生命周期调用顺序和父,子组件的生命周期顺序是一致的。类似堆栈调用(先入后出

在这里插入图片描述

function proxyHOC(WrappedComponent) {
  return class extends Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  }
}
//使用示例
class Example extends Component {
  render() {
    return <input name="name" {...this.props.name} />;
  }
}
export default HOC(Example)
复制代码

通过属性代理实现的HOC可具有以下功能:

(1)操作props

可以对传入组件的props进行增加、修改、删除或者根据特定的props进行特殊的操作。

注意,使用HOC包裹后的组件,在给组件传入props时实际传入到了HOC的container容器组件中,如不需要操作props,请务必在容器组件中将props再度传给传入组件,否则传入组件不会接收到props

function proxyHOC(WrappedComponent) {
  return class Container extends Component {
    render() {
      const newProps = {
        ...this.props,
        user: "ConardLi"
      }
      return <WrappedComponent {...newProps} />;
    }
  }
}

复制代码

(2)获取refs引用

高阶组件中可获取传入组件的ref,通过ref获取组件的实例(即拿到传入组件实例的this),如下面的代码,当程序初始化完成后调用原组件的log方法。

function refHOC(WrappedComponent) {
  return class Container extends Component {
    componentDidMount() {
      this.wapperRef.log()
    }
    render() {
      return <WrappedComponent {...this.props} ref={ref => { this.wapperRef = ref }} />;
    }
  }
}

复制代码

注意:HOC包裹的组件默认无法在外部调用时拿到原组件refs引用

虽然高阶组件的约定是将所有 props 传递给被包装组件,但这对于 refs 并不适用。那是因为 ref 实际上并不是一个 prop。就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。

这个问题的解决方案是通过使用 React.forwardRef API(React 16.3 中引入)。

(3)抽象state

// 高阶组件
function HOC(WrappedComponent) {
  return class Container extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        name: "",
      };
      this.onChange = this.onChange.bind(this);
    }
    
    onChange = (event) => {
      this.setState({
        name: event.target.value,
      })
    }
    
    render() {
      const newProps = {
        name: {
          value: this.state.name,
          onChange: this.onChange,
        },
      };
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  };
}

// 使用
class Example extends Component {
  render() {
    return <input name="name" {...this.props.name} />;
  }
}
export default HOC(Example)
//或者
@HOC
class Example extends Component {
  render() {
    return <input name="name" {...this.props.name} />;
  }
}

复制代码

在这个例子中,我们把 input 组件中对 name这个prop在高阶组件中进行了重定义的覆盖(用value和onChange 代替),这就有效地抽象了同样的 state 操作。使得input组件由非受控组件变成了受控组件

(4)操作组件的static方法

可以对传入组件的static静态方法进行获取调用,增加、修改、删除

function refHOC(WrappedComponent) {
  return class Container extends Component {
    componentDidMount() {
    //获取static方法
      console.log(WrappedComponent.staticMethod)
    }
    //新增static方法
    WrappedComponent.addMethod1=()=>{}
    
    render() {
      return <WrappedComponent {...this.props} ref={ref => { this.wapperRef = ref }} />;
    }
  }
}

复制代码

但当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着容器组件默认没有传入组件的任何静态方法,即无法在其他地方引入组件时拿到其静态方法。所以与props同理,请务必将传入组件的静态方法拷贝到容器组件上

(5)根据props实现条件渲染

根据特定的props决定传入组件是否渲染(如最上面的基本HOC例子)

function visibleHOC(WrappedComponent) {
  return class extends Component {
    render() {
      if (this.props.visible === false) return null;
      return <WrappedComponent {...props} />;
    }
  }
}

复制代码

(6)用其他元素包裹传入的组件

在HOC的容器组件中将原组件通过其他元素再包裹起来,从而实现布局或者修改样式的目的:

function withBackgroundColor(WrappedComponent) {
    return class extends React.Component {
        render() {
            return (
                <div style={{ backgroundColor: "#ccc" }}>
                    <WrappedComponent {...this.props} {...newProps} />
                </div>
            );
        }
    };
}

复制代码

2.2 反向继承

返回一个组件,该组件继承传入组件,在render中调用原组件的render。

由于继承了原组件,能通过this访问到原组件的生命周期、props、state、render等,相比属性代理它能操作更多的属性。

这种实现方式下,HOC组件和传入组件的生命周期调用顺序与队列类似(先进先出在这里插入图片描述

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

复制代码

通过反向继承实现的HOC,相比属性代理具有以下额外的功能:

(1)渲染劫持

渲染劫持指的就是高阶组件可以控制 WrappedComponent 的渲染过程,并渲染各种各样的结果。我们可以在这个过程中在任何 React 元素输出的结果中读取、增加、修改、删除 props,或读取或修改 React 元素树,或条件显示元素树,又或是用样式控制包裹元素树。

上面属性代理提到的条件渲染,其实也是渲染劫持的一种实现。

如果元素树中包括了函数类型的 React 组件,就不能操作组件的子组件

渲染劫持实示例:

function hijackHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      const tree = super.render();
      let newProps = {};
      if (tree && tree.type === "input") {
        newProps = { value: "渲染被劫持了" };
      }
      const props = Object.assign({}, tree.props, newProps);
      const newTree = React.cloneElement(tree, props, tree.props.children);
      return newTree;
    }
  }
}

复制代码

(2)劫持传入组件生命周期

因为反向继承方式实现的高阶组件返回的新组件是继承于传入组件,所以当新组件定义了同样的方法时,将会会覆盖父类(传入组件)的实例方法,如下面代码所示:

function HOC(WrappedComponent){
  // 继承了传入组件
  return class HOC extends WrappedComponent {
    // 注意:这里将重写 componentDidMount 方法
    componentDidMount(){
      ...
    }
    render(){
      //使用 super 调用传入组件的 render 方法
      return super.render();
    }
  }
}

复制代码

(3)操作传入组件state

反向继承方式实现的高阶组件中可以读取、编辑和删除传入组件实例中的 state,如下面代码所示:

function debugHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      console.log("props", this.props);
      console.log("state", this.state);
      return (
        <div className="debuging">
          {super.render()}
        </div>
      )
    }
  }
}

复制代码

操作传入组件的state可能会让 WrappedComponent 组件内部状态变得一团糟。大部分的高阶组件都应该限制读取或增加 state,尤其是后者,可以通过重新命名 state,以防止混淆。

2.3 两种方式对比

  • 属性代理是从“组合”的角度出发,这样有利于从外部去操作 WrappedComponent,可以操作的对象是 props,或者在 WrappedComponent 外面加一些拦截器,控制器等。
  • 反向继承则是从“继承”的角度出发,是从内部去操作 WrappedComponent,也就是可以操作组件内部的 state ,生命周期,render函数等等。

在这里插入图片描述

3. 高阶组件实际应用

(1)逻辑复用

多个页面组件存在代码结构和需求相似的情况,只是一些传参和数据不同,存在较多重复性代码。使用高阶组件进行统一包裹封装即可

下面是两个结构和需求相似的页面组件:

// views/PageA.js
import React from "react";
import fetchMovieListByType from "../lib/utils";
import MovieList from "../components/MovieList";

class PageA extends React.Component {
  state = {
    movieList: [],
  }
  /* ... */
  async componentDidMount() {
    const movieList = await fetchMovieListByType("comedy");
    this.setState({
      movieList,
    });
  }
  render() {
    return <MovieList data={this.state.movieList} emptyTips="暂无喜剧"/>
  }
}
export default PageA;

复制代码
// views/PageB.js
import React from "react";
import fetchMovieListByType from "../lib/utils";
import MovieList from "../components/MovieList";

class PageB extends React.Component {
  state = {
    movieList: [],
  }
  // ...
  async componentDidMount() {
    const movieList = await fetchMovieListByType("action");
    this.setState({
      movieList,
    });
  }
  render() {
    return <MovieList data={this.state.movieList} emptyTips="暂无动作片"/>
  }
}
export default PageB;


复制代码

将重复逻辑抽离成一个HOC:

// HOC
import React from "react";
const withFetchingHOC = (WrappedComponent, fetchingMethod, defaultProps) => {
  return class extends React.Component {
    async componentDidMount() {
      const data = await fetchingMethod();
      this.setState({
        data,
      });
    }
    
    render() {
      return (
        <WrappedComponent 
          data={this.state.data} 
          {...defaultProps} 
          {...this.props} 
        />
      );
    }
  }
}

复制代码

使用示例:

// 使用:
// views/PageA.js
import React from "react";
import withFetchingHOC from "../hoc/withFetchingHOC";
import fetchMovieListByType from "../lib/utils";
import MovieList from "../components/MovieList";
const defaultProps = {emptyTips: "暂无喜剧"}

export default withFetchingHOC(MovieList, fetchMovieListByType("comedy"), defaultProps);

// views/PageB.js
import React from "react";
import withFetchingHOC from "../hoc/withFetchingHOC";
import fetchMovieListByType from "../lib/utils";
import MovieList from "../components/MovieList";
const defaultProps = {emptyTips: "暂无动作片"}

export default withFetchingHOC(MovieList, fetchMovieListByType("action"), defaultProps);;

// views/PageOthers.js
import React from "react";
import withFetchingHOC from "../hoc/withFetchingHOC";
import fetchMovieListByType from "../lib/utils";
import MovieList from "../components/MovieList";
const defaultProps = {...}

export default withFetchingHOC(MovieList, fetchMovieListByType("some-other-type"), defaultProps);

复制代码

上面设计的高阶组件 withFetchingHOC,把不一样的部分(组件和获取数据的方法) 抽离到外部作为传入,从而实现页面的复用。

(2)权限控制

function auth(WrappedComponent) {
  return class extends Component {
    render() {
      const { visible, auth, display = null, ...props } = this.props;
      if (visible === false || (auth && authList.indexOf(auth) === -1)) {
        return display
      }
      return <WrappedComponent {...props} />;
    }
  }
}
复制代码

authList是我们在进入程序时向后端请求的所有权限列表,当组件所需要的权限不在传入权限列表中,或者设置的 visible是false,我们将其显示为传入的组件样式,或者null。我们可以将任何需要进行权限校验的组件应用HOC:

  @auth
  class Input extends Component {  ...  }
  @auth
  class Button extends Component {  ...  }

  <Button auth="user/addUser">添加用户</Button>
  <Input auth="user/search" visible={false} >添加用户</Input>

复制代码

4. 其他技巧:

(1)高阶组件参数

有时,我们调用高阶组件时需要传入一些参数,这可以用非常简单的方式来实现:


import React, { Component } from "React"; 
function HOCFactoryFactory(...params) { 
 // 可以做一些改变 params 的事
 return function HOCFactory(WrappedComponent) { 
	 return class HOC extends Component { 
	 render() { 
	 return <WrappedComponent {...this.props} />; 
 		} 
 	} 
 } 
} 
复制代码

当你使用的时候,可以这么写:

HOCFactoryFactory(params)(WrappedComponent) 
// 或者
@HOCFatoryFactory(params) 
class WrappedComponent extends React.Component{}
复制代码

(2)高阶组件命名

当包裹一个高阶组件时,我们失去了原始 WrappedComponent 的 displayName,而组件名字是方便我们开发与调试的重要属性

HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`; 
// 或者
class HOC extends ... { 
 static displayName = `HOC(${getDisplayName(WrappedComponent)})`; 
 ... 
复制代码

然后通过HOC.displayName来获取即可

推荐文章
树成林  ·  人生掌控实验——早起训练营第2期  ·  1 年前  
Pan式爱美哲学  ·  祝福如下图,2023也请多多关照。 大小姐们 ...  ·  1 年前  
夭小遥  ·  摔跤吧爸爸  ·  3 年前  
伯乐在线  ·  面向对象:做个有趣的很酷的很拽的俗人  ·  4 年前  
© 2022 51好读
删除内容请联系邮箱 2879853325@qq.com