React 重温之高阶组件(HOC)

什么是高阶组件

话很少说,先看官方释义:html

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

其实,高阶组件指的是函数接收一个组件后,最终返回的那个新组件。由于这个新组件把咱们当作参数传入的组件给包裹在内,相对于咱们传入的组件来讲,这个返回的新的组件就是“高阶组件”了。程序员

干啥这么麻烦

咱们都知道,React让咱们抽象出一些可复用的组件从而减小前端工做量,通常状况下咱们只须要定义一些组件,而后把他们组装成一个组件树就行了,为啥还要弄一个函数来去包裹组件呢? 算法

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

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

Show me the code

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

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

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组件用来展现你的博客文章:code

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

参考连接
参考连接

相关文章
相关标签/搜索