React 進階之高階組件

高階組件 HOC

高階組件(HOC)是react中的高級技術,用來重用組件邏輯。但高階組件自己並非React API。它只是一種模式,這種模式是由react自身的組合性質必然產生的。javascript

具體而言,高階組件就是一個函數,且該函數接受一個組件做爲參數,並返回一個新的組件。前端

語法

const EnhancedComponent = higherOrderComponent(WrappedComponent);
複製代碼

一般咱們寫的都是對比組件,那什麼是對比組件呢?對比組件將 props 屬性轉變成 UI,高階組件則是將一個組件轉換成另外一個組件。java

應用場景

高階組件在 React 第三方庫中很常見,好比 Reduxconnect 方法和 RelaycreateContainerreact

意義何在

以前用混入(mixins)技術來解決橫切關注點。但是混入(mixins)技術產生的問題要比帶來的價值大。因此就移除混入(mixins)技術,對於如何轉換你已經使用了混入(mixins)技術的組件,可查看更多資料。顯然,橫切關注點就用高階組件(HOC)來解決了。git

示例

1.假設有一個評論組件(CommentList),該組件從外部數據源訂閱數據並渲染github

// CommentList.js
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> ); } } 複製代碼

2.而後,有一個訂閱單個博客文章的組件(BlogPost)算法

// BlogPost.js
class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id)
    }
  }

  conponentDidMount() {
    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} />; } } 複製代碼

3.以上,評論組件 CommentList 和 文章訂閱組件 BlogPost 有如下不一樣數組

  • 調用 DataSource 的方法不一樣
  • 渲染的輸出不一樣

但是,它們也有相同點app

  • 掛載組件時,向 DataSource 添加一個改變的監聽器;
  • 在監聽器內,當數據源改變時,就調用 setState;
  • 卸載組件時,移除改變監聽器;

一個大型應用中,從 DataSource 訂閱數據並調用 setState 的模式會屢次使用,這個時候做爲前端就會嗅出代碼要整理一下,可以抽出相同的地方做爲一個抽象,而後許多組件可共享它,這就是高階組件產生的背景。函數

4.咱們使用個函數 withSubscription 讓它完成如下功能

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

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);
複製代碼

上面函數 withSubscription 的第一個參數是咱們以前寫的兩個組件,第二個參數檢索所須要的數據(DataSource 和 props)。

那這個函數 withSubscription 該怎麼寫呢?

const withSubscription = (TargetComponent, 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);
    }

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

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

    render(){
      return <TargetComponent data={this.state.data} {...this.props} /> } } } 複製代碼

5.總結下

  • 高階組件既不會修改參數組件,也不使用繼承拷貝它的行爲,簡單來講就是一個沒有反作用的純函數
  • 被包裹組件接收容器的全部 props 屬性以及新的數據 data 用於渲染輸出。高階組件並不關心數據的使用方式,被包裹組件不關心數據來源。
  • 高階組件和被包裹組件的合約在於 props 屬性。這就是能夠替換另外一個高階組件,只要他們提供相同的 props 屬性給被包裹組件便可。你能夠把高階組件當成一套主題皮膚。

不改變原始組件,使用組合

如今,咱們對高階組件已經有了初步認識,但是實際業務當中,咱們寫高階組件時,容易寫着寫着就修改了組件的內容,千萬要抵住誘惑。好比

const logProps = (WrappedComponent) => {
  WrappedComponent.prototype.componentWillReceiveProps = function(nextProps) {
    console.log('CurrentProps', this.props);
    console.log('NextProps', nextProps);
  }
  return WrappedComponent;
}

const EnhancedComponent = logProps(WrappedComponent);
複製代碼

上面高階組件 logProps 有幾個問題

  • 被包裹組件 WrappedComponent 不能獨立於加強型組件(enhanced component)被重用。
  • 若是你在 EnhancedComponent 上應用另外一個高階組件 logProps2,一樣也會改去改變 componentWillReceiveProps,高階組件 logProps 的功能就會被覆蓋。
  • 這樣的高階組件對沒有生命週期的函數式組件是無效的

針對以上問題,要想達到同等效果,可以使用組合方式

const logProps = (WrappedComponent) => {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }

    render() {
      // 用容器包裹輸入組件,不要修改它,漂亮!
      return <WrappedComponent {...this.props} />; } } } 複製代碼

聯想

不知道你發現沒有,高階組件和容器組件模式有相同之處。

  • 容器組件專一於在高層和低層之間進行責任分離策略的一部分,傳遞 props 屬性給子組件;
  • 高階組件使用容器做爲它們實現的一部分,可理解高階組件就是參數化的容器組件定義。

約定:貫穿傳遞不相關props屬性給被包裹的組件

高階組件返回的那個組件與被包裹的組件具備相似的接口。

render(){
  // 過濾掉專用於這個高階組件的 props 屬性,丟棄 extraProps
  const { extraProps, ...restProps } = this.props;

  // 向被包裹的組件注入 injectedProps 屬性,這些通常都是狀態值或實例方法
  const injectedProps = {
    // someStateOrInstanceMethod
  };

  return (
    <WrappedComponent injectedProps={injectedProps} {...restProps} /> ) } 複製代碼

約定幫助確保高階組件最大程度的靈活性和可重用性。

約定:最大化的組合性

並非全部的高階組件看起來都是同樣的。有時,它們僅接收單獨一個參數,即被包裹的組件:

const NavbarWithRouter = withRouter(Navbar);
複製代碼

通常而言,高階組件會接收額外的參數。在下面這個來自 Relay 的示例中,一個 config 對象用於指定組件的數據依賴:

const CommentWithRelay = Relay.createContainer(Comment, config);
複製代碼

高階組件最多見簽名以下所示:

const ConnectedComment = connect(commentSelector, commentActions)(Comment);
複製代碼

能夠這麼理解

// connect 返回一個函數(高階組件)
const enhanced = connect(commentSelector, commentActions);
const ConnectedComment = enhanced(Comment);
複製代碼

換句話說,connect 是一個返回高階組件的高階函數!可是這種形式多少讓人有點迷惑,可是它有一個性質,只有一個參數的高階函數(connect 函數返回的),返回是 Component => Component,這樣就可讓輸入和輸出類型相同的函數組合在一塊兒,在一塊兒,在一塊兒

// 反模式
const EnhancedComponent = withRouter(connect(commentSelector, commentActions)(Comment));

// 正確模式
// 你可使用一個函數組合工具
// compose(f, g, h) 和 (...args) => f(g(h(...args)))是同樣的
const enhanced = compose(
  withRouter,
  connect(commentSelector, commentActions)
);
const EnhancedComponent = enhanced(Comment);
複製代碼

包括 lodash(好比說 lodash.flowRight), Redux 和 Ramda 在內的許多第三方庫都提供了相似 compose 功能的函數。

約定:包裝顯示名字以便於調試

若是你的高階組件名字是 withSubscription,且被包裹的組件的顯示名字是 CommentList,那麼就是用 WithSubscription(CommentList) 這樣的顯示名字:

const withSubscription = (WrappedComponent) => {
  // return class extends React.Component { /* ... */ };

  class WithSubscription extends React.Component { /* ... */ };
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`
  return WithSubscription;
}

const getDisplayName = (WrappedComponent) => {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
複製代碼

有幾個不要作的事

不要在render方法內使用高階組件

**React的差分算法(稱爲協調)**使用組件標識肯定是否更新現有的子樹或扔掉它並從新掛載一個新的。若是 render 方法返回的組件和前一次渲染返回的組件是徹底相同的(===),React就遞歸地更新子樹,這是經過差分它和新的那個完成。若是它們不相等,前一個子樹被徹底卸載掉。

通常而言,你不須要考慮差分算法的原理。可是它和高階函數有關。由於它意味着你不能在組件的 render 方法以內應用高階函數到組件:

render() {
  // 每一次渲染,都會建立一個新的EnhancedComponent版本
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // 就會引發每一次都會使子對象樹徹底被卸載/從新加載
  return <EnhancedComponent />; } 複製代碼

上面代碼會致使的問題

  • 性能問題
  • 從新加載一個組件會引發原有組件的狀態和它的全部子組件丟失

必須將靜態方法作拷貝

問題:當你應用一個高階組件到一個組件時,儘管,原始組件被包裹於一個容器組件內,也就意味着新組件會沒有原始組件的任何靜態方法。

// 定義靜態方法
WrappedComponent.staticMethod = function() {/*...*/}
// 使用高階組件
const EnhancedComponent = enhance(WrappedComponent);

// 加強型組件沒有靜態方法
typeof EnhancedComponent.staticMethod === 'undefined' // true
複製代碼

解決方案: (1)能夠將原始組件的方法拷貝給容器

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // 必須得知道要拷貝的方法 :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}
複製代碼

(2)這樣作,就須要你清楚的知道都有哪些靜態方法須要拷貝。你可使用 hoist-non-react-statics 來幫你自動處理,它會自動拷貝全部非React的靜態方法:

import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}
複製代碼

(3)另一個可能的解決方案就是分別導出組件自身的靜態方法。

// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...export the method separately...
export { someFunction };

// ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';
複製代碼

refs 屬性不能貫穿傳遞

通常來講,高階組件能夠傳遞全部的 props 屬性給包裹的組件,可是不能傳遞 refs 引用。由於並非像 key 同樣,refs 是一個僞屬性,React 對它進行了特殊處理。若是你向一個由高階組件建立的組件的元素添加 ref 應用,那麼 ref 指向的是最外層容器組件實例的,而不是被包裹的組件。

React16版本提供了 React.forwardRef 的 API 來解決這一問題,可在 refs 傳遞章節中瞭解下。

你還能夠

React 源碼解析之嘮叨兩句

React 源碼解析之總覽

相關文章
相關標籤/搜索