React HOC高阶组件详解

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

内容简介: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 方法来解决这个问题,具体可以参考这里


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

查看所有标签

猜你喜欢:

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

Is Parallel Programming Hard, And, If So, What Can You Do About

Is Parallel Programming Hard, And, If So, What Can You Do About

Paul E. McKenney

The purpose of this book is to help you understand how to program shared-memory parallel machines without risking your sanity.1 By describing the algorithms and designs that have worked well in the pa......一起来看看 《Is Parallel Programming Hard, And, If So, What Can You Do About 》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

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

在线图片转Base64编码工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具