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

參考連接
參考連接

相關文章
相關標籤/搜索