React HOC高阶组件详解

栏目: IOS · Android · 发布时间: 6年前

内容简介:High Order Component(包装组件,后面简称HOC),是React开发中提高组件复用性的高级技巧。HOC并不是React的API,他是根据React的特性形成的一种开发模式。HOC具体上就是一个接受组件作为参数并返回一个新的组件的方法在React的第三方生态中,有非常多的使用,比如Redux的

High Order Component(包装组件,后面简称HOC),是React开发中提高组件复用性的高级技巧。HOC并不是React的API,他是根据React的特性形成的一种开发模式。

HOC具体上就是一个接受组件作为参数并返回一个新的组件的方法

const EnhancedComponent = higherOrderComponent(WrappedComponent)
复制代码

在React的第三方生态中,有非常多的使用,比如Redux的 connect 方法或者React-Router的 withrouter 方法。

举个例子

我们有两个组件:

// CommentList
class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource" is some global data source
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // Subscribe to changes
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // Clean up listener
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // Update component state whenever the data source changes
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}
复制代码
// BlogPost
class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id)
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

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

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id)
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}
复制代码

他们虽然是两个不同的组件,对DataSource的需求也不同,但是他们有很多的内容是相似的:

  • 在组件渲染之后监听DataSource
  • 在监听器里面调用setState
  • 在unmout的时候删除监听器

在大型的工程开发里面,这种相似的代码会经常出现,那么如果有办法把这些相似代码提取并复用,对工程的可维护性和开发效率可以带来明显的提升。

使用HOC我们可以提供一个方法,并接受不了组件和一些组件间的区别配置作为参数,然后返回一个包装过的组件作为结果。

function withSubscription(WrappedComponent, selectData) {
  // ...and returns another component...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ... that takes care of the subscription...
      DataSource.addChangeListener(this.handleChange);
    }

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

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

    render() {
      // ... and renders the wrapped component with the fresh data!
      // Notice that we pass through any additional props
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}
复制代码

然后我们就可以通过简单的调用该方法来包装组件:

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);
复制代码

注意:在HOC中我们并没有修改输入的组件,也没有通过继承来扩展组件。HOC是通过组合的方式来达到扩展组件的目的,一个HOC应该是一个没有副作用的方法。

在这个例子中我们把两个组件相似的生命周期方法提取出来,并提供selectData作为参数让输入组件可以选择自己想要的数据。因为withSubscription是个纯粹的方法,所以以后如果有相似的组件,都可以通过该方法进行包装,能够节省非常多的重复代码。

不要修改原始组件,使用组合进行功能扩展

function logProps(InputComponent) {
  InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
    console.log('Current props: ', this.props);
    console.log('Next props: ', nextProps);
  };
  // The fact that we're returning the original input is a hint that it has
  // been mutated.
  return InputComponent;
}

// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);
复制代码

通过以上方式我们也可以达到扩展组件的效果,但是会存在一些问题

componentWillReceiveProps

修改原始组件的方式缺乏抽象化,使用者必须知道这个方法是如何实现的来避免上面提到的问题。

如果通过组合的方式来做,我们就可以避免这些问题

function logProps(InputComponent) {
  return class extends React.Component{
    componentWillReceiveProps(nextProps) {
        console.log('Current props: ', this.props);
        console.log('Next props: ', nextProps);
    }
    render() {
        <InputComponent {...this.props} />
    }
  }
}

// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);
复制代码

惯例:无关的props传入到原始组件

HOC组件会在原始组件的基础上增加一些扩展功能使用的props,那么这些props就不应该传入到原始组件(当然有例外,比如HOC组件需要使用原始组件指定的props),一般来说我们会这样处理props:

render() {
  // Filter out extra props that are specific to this HOC and shouldn't be
  // passed through
  const { extraProp, ...passThroughProps } = this.props;

  // Inject props into the wrapped component. These are usually state values or
  // instance methods.
  const injectedProp = someStateOrInstanceMethod;

  // Pass props to wrapped component
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}
复制代码

extraProp 是HOC组件中要用的props,不用的剩下的props我们都认为是原始组件需要使用的props,如果是两者通用的props你可以单独传递。

惯例:包装组件的显示名称来方便调试

function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
复制代码

简单来说就是通过手动指定 displayName 来让HOC组件能够更方便得被react devtool观察到

惯例:不要在render方法里面调用HOC方法

render() {
  // A new version of EnhancedComponent is created on every render
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // That causes the entire subtree to unmount/remount each time!
  return <EnhancedComponent />;
}
复制代码

一来每次调用 enhance 返回的都是一个新的class,react的diffing算法是根据组件的特征来判断是否需要重新渲染的,如果两次render的时候组件之间不是(===)完全相等的,那么会直接重新渲染,而部署根据props传入之后再进行diff,对性能损耗非常大。并且重新渲染会让之前的组件的state和children全部丢失。

二来React的组件是通过props来改变其显示的,完全没有必要每次渲染动态产生一个组件,理论上需要在渲染时自定义的参数,都可以通过事先指定好props来实现可配置。

静态方法必须被拷贝

有时候会在组件的class上面外挂一下帮助方法,如果按照上面的方法进行包装,那么包装之后的class就没有来这些静态方法,这时候为了保持组件使用的一致性,一般我们会把这些静态方法拷贝到包装后的组件上。

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // Must know exactly which method(s) to copy :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}
复制代码

这个之适用于你已知输入组件存在那些静态方法的情况,如果需要可扩展性更高,那么可以选择使用第三方插件 hoist-non-react-statics

import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}
复制代码

ref

ref作为React中的特殊属性--类似于key,并不属于props,也就是说我们使用传递props的方式并不会把ref传递进去,那么这时候如果我们在HOC组件上放一个ref,拿到的是包装之后的组件而不是原始组件,这可能就会导致一些问题。

在React 16.3之后官方增加来一个 React.forwardRef 方法来解决这个问题,具体可以参考这里


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

产品经理必懂的技术那点事儿:成为全栈产品经理

产品经理必懂的技术那点事儿:成为全栈产品经理

唐韧 / 电子工业出版社 / 2018-1 / 59

《产品经理必懂的技术那点事儿:成为全栈产品经理》以非技术背景产品经理学习技术为主题,将技术知识以简单并且易于理解的方式讲述出来,帮助非技术背景产品经理了解技术、学习技术,旨在帮助产品经理高效地与技术人员进行沟通与合作,避免不懂技术带来的困扰。 《产品经理必懂的技术那点事儿:成为全栈产品经理》主要内容围绕产品经理需要了解的互联网基础技术知识展开,涉及客户端、服务器端、数据库及一些数据处理知识。......一起来看看 《产品经理必懂的技术那点事儿:成为全栈产品经理》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码