React hoc模式

99 阅读4分钟

高阶组件

高阶组件是一个函数,传入一个源组件,并且返回一个包装后的组件。高阶组件用于强化源组件功能,并且分离源组件和包装组件,使代码能够复用。是装饰器模式的一种实现。

HOC经常应用在React 的一些第三方库中,比如Redux的connect 和Relay的createFragmentContainer

将HOC应用于横切关注点(Cross-Cutting Concerns)

横切关注点

横切关注点(cross-cutting concern)是指在面向切面(或方向)的编程中,在应用程序中影响多个模块的代码块,但是这些代码块又不能清晰的从模块中分离出来,导致代码重复。面向过程和函数式编程是是整个的过程调用,不能同时实现两个目标(要实现的功能和相关的横切关注点)。面向切面的编程旨在将横切关注点封装到方面中以保持模块化。这允许对解决横切问题的代码进行干净的隔离和重用。通过基于横切关注点的设计,使代码模块化和简化代码的维护,软件工程设计的更好。

例如,如果写一个处理医疗记录的应用,医疗记录是一个核心的关乎点,然而,把更改医疗记录的日志写到数据库,或是认证系统是横切关注点,这些横切关注点与英勇的其他许多模块交互。

组件是React中代码重用的主要单元。然而,您会发现有些场景并不直接适合传统组件。

例子

假如有一个评论列表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>
    );
  }
}

稍后,您编写了一个组件来订阅单个博客文章,它遵循类似的模式:

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} />;
  }
}

CommentList和BlogPost并不完全相同——它们在DataSource上调用不同的方法,并且它们呈现不同的输出。但它们的大部分实现都是一样的:

  • 在挂载时,添加一个DataSource监听器。
  • 在监听听器内部,只要数据源发生更改,就调用setState。
  • 卸载时,删除监听器。

你可以想象,在一个大的应用程序中,订阅DataSource和调用setState的相同代码会一次又一次地发生。我们需要一个抽象,允许我们在一个地方定义这个逻辑,并在许多组件之间共享它。这就是高阶组件的优势所在。 我们定义一个函数withSubscription,函数的第一个参数为源组件,第二个参数为源组件获取订阅的数据类型函数。

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

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

当CommentListWithSubscription和BlogPostWithSubscription被渲染时,把DataSource对应的值传递给CommentList和BlogPost。

// This function takes a component...
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} />;
    }
  };
}

注意,HOC不修改源组件,也不使用继承来复制其行为。相反,HOC通过将源组件包装在容器组件中来组合源组件。HOC是一个没有副作用的纯函数。 源组件接收所有的包装组件的props,还有包装组件用于渲染输出一些新的prop data。HOC不用关心数据如何用,源组件不用关心数据来自哪里。 与组件一样,withSubscription和包装组件之间的契约完全是基于props的。这使得将一个HOC替换为另一个HOC变得很容易,只要它们为包装的组件提供相同的props。例如,如果您更改了数据获取库,这可能会很有用。

不要改变原始组件,使用组合

抵制在HOC中修改组件原型。

function logProps(InputComponent) {
  InputComponent.prototype.componentDidUpdate = function(prevProps) {
    console.log('Current props: ', this.props);
    console.log('Previous props: ', prevProps);
  };
  // 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);

这里有一些问题。其一是输入组件不能与增强组件分开重用。更关键的是,如果你将另一个HOC应用到EnhancedComponent,它也会改变componentdiduupdate,那么第一个HOC的功能将被覆盖!这个HOC也不能用于没有生命周期方法的功能组件。

突变的hoc是一种有泄漏的抽象——消费者必须知道它们是如何实现的,以避免与其他hoc发生冲突。

hoc应该使用组合,而不是突变,将输入组件包装在容器组件中:

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('Current props: ', this.props);
      console.log('Previous props: ', prevProps);
    }
    render() {
      // Wraps the input component in a container, without mutating it. Good!
      return <WrappedComponent {...this.props} />;
    }
  }
}

参考文献:

  1. 「React 进阶」 学好这些 React 设计模式,能让你的 React 项目飞起来
  2. Higher-Order Components
  3. Cross-cutting concern