React 重温之高阶组件(HOC)

什么是高阶组件

话不多说,先看官方释义:

Concretely, a higher-order component is a function that takes a component and returns a new component.

上面这段话,已经很清楚明白的告诉我们高阶组件是什么,以及高阶组件是干啥的。a higher-order component is a function告诉我们说高阶组件是一个函数(function),是一个什么函数呢? takes a component and returns a new component.是一个接收一个组件作为参数,最终返回一个新组件的函数。

所以说,高阶组件并不是一个“组件”,而是一个函数,叫“高阶函数”可能更加合适一些,但高阶函数这个名字被人占用了,高阶函数是以函数为参数,最终返回一个新函数的函数。那为什么又要加高阶组件呢?这个高阶组件具体指的是什么东西呢?

其实,高阶组件指的是函数接收一个组件后,最终返回的那个新组件。因为这个新组件把我们当做参数传入的组件给包裹在内,相对于我们传入的组件来说,这个返回的新的组件就是“高阶组件”了。

干啥这么麻烦

我们都知道,React让我们抽象出一些可复用的组件从而减少前端工作量,一般情况下我们只需要定义一些组件,然后把他们组装成一个组件树就好了,为啥还要弄一个函数来去包裹组件呢?

其实呢,归根结底,都是因为懒。。。因为我们懒得一遍遍写相同的代码,我们把具有相同逻辑的内容抽象成一个组件,一次定义,到处可用;同样因为懒,我们把具有类似功能的组件抽象,用一个新的组件去包裹它,把相同的部分放到包裹组件里,不同的部分放到各自原本组件里,那么这个新的用来包裹我们类似组件的新组件,就是“高阶组件”了。

说到底,我们在业务逻辑的基础上完成一次抽象过程,得到一个个组件;在组件的基础再做一次抽象,得到一个高阶组件(高阶函数)。

Show me the code

闲话少说,让我们来看下官方的示例:

首先是一个CommentList组件,这个组件从外部数据源订阅数据并展示评论列表:

class CommentList extends React.Component {
  constructor() {
    super();
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource" 就是全局的数据源
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // 添加事件处理函数订阅数据
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // 清除事件处理函数
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // 任何时候数据发生改变就更新组件
    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} />;
  }
}

从这两个组件的代码上来看,我们很容易就可以发现一个问题:他俩长的太像了。。。这不都是监听外部数据源,有变动了就更新自己的state,然后把数据按照各自的逻辑渲染出来嘛。唯一不一样的地方就是每个组件需要的数据和渲染方式不一样。

作为一个以出名的程序员,看到这样的组件,你很可能已经想把他们相同的东西拿出来放到一个地方,只保留各自不同的部分,不然谁知道以后业务逻辑变化了,还有多少类似的组件等着你,难道要把重复的代码到处写吗?Don‘t Repeat Yourself!

OK,如果你这么想了,那就很靠近高阶组件的思想了,下面就是针对上面的组件,官方给出的高阶组件:

function withSubscription(WrappedComponent, selectData) {
  // ……返回另一个新组件……
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ……注意订阅数据……
      DataSource.addChangeListener(this.handleChange);
    }

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

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

    render() {
      // ……使用最新的数据渲染组件
      // 注意此处将已有的props属性传递给原组件
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

我们看到,withSubscription是一个函数,接收WrappedComponent, selectData两个参数,最终返回一个新的组件。在新组件的render()函数里,直接返回了WrappedComponent这个被包裹的组件。在handleChange函数里,使用selectData函数来筛选被包裹组件需要的数据。

我们上面说到,BlogPost和CommentList这两个组件除了需要的数据和渲染数据的方式不同外,其它基本都一样,于是在withSubscription函数里,我们把传入组件原封不动的渲染,在筛选数据的时候,使用传入的selectData函数来筛选,于是withSubscription这个函数就可以很容易的返回一个高阶组件来包裹 需要不同数据和渲染方式 的组件。

使用方式如下:

//首先简化组件定义

class CommentList extends React.Component {
  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

class BlogPost extends React.Component {
  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}
//去包裹组件

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()//自定义筛选数据
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)//自定义筛选数据
);

以上就是所有代码,我们把原来BlogPost和CommentList组件中重复的代码都放到包裹组件里,只保留各自不同的部分,然后调用高阶组件函数来生成CommentListWithSubscription和BlogPostWithSubscription这两个组件,之后在需要用到BlogPost和CommentList组件的地方都用CommentListWithSubscription和BlogPostWithSubscription来替换就好了。

好像哪里不太对

看完上面的官方示例后,如果你感觉好像哪里不太对,那么恭喜你,你基本上算是一个React高手了

那么到底是哪里不太对呢?细心的朋友可能已经发现了,我们在比较两个被包裹组件的时候提到,两个组件 需要不同数据和渲染方式,渲染方式是每个组件最核心的功能,这个没法变动,可是数据有两个来源啊,为啥非要从state里拿数据?

我们完全可以把数据来源从组件内部的state拿到外部的props里啊,这一样一来同样可以简化组件的代码啊!

然而事情并没有那么简单,我们之前提到,这些组件的数据来自 外部数据源,如果我们把数据来源从state迁移到props,同样需要在使用组件的地方去筛选数据,并没有减少这个工作量,只是把这个工作量从组件内部移到使用组件的地方罢了。。。

注意

不要在render函数中使用高阶组件

React使用的差异算法(称为协调)使用组件标识确定是否更新现有的子对象树或丢掉现有的子树并重新挂载。如果render函数返回的组件和之前render函数返回的组件是相同的,React就递归的比较新子对象树和旧子对象树的差异,并更新旧子对象树。如果他们不相等,就会完全卸载掉旧的之对象树。

在render使用高阶组件,其实就是调用函数生成一个高阶组件,基本每次render都会生成一个新的组件,这个就比较。。。

如果确实需要动态的调用高阶组件,一个比较合理的方式是在组件的构造函数或生命周期函数中调用。

必须将静态方法做拷贝

使用高阶组件包装组件,原始组件被容器组件包裹,也就意味着新组件会丢失原始组件的所有静态方法。

决这个问题的方法就是,将原始组件的所有静态方法全部拷贝给新组件:

Refs属性不能传递

一般来说,高阶组件可以传递所有的props属性给包裹的组件,但是不能传递refs引用。因为并不是像key一样,refs是一个伪属性,React对它进行了特殊处理。如果你向一个由高阶组件创建的组件的元素添加ref应用,那么ref指向的是最外层容器组件实例的,而不是包裹组件。

具体可以参考React 重温之 Refs

参考链接
参考链接

    原文作者:紫日残月
    原文地址: https://segmentfault.com/a/1190000015273513
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞