React高階組件HOC

高階組件

  1. 爲何須要高階組件? 高階組件是一個函數,接收一個組件,而後返回一個新的組件。
    • 這個問題很簡單,爲何咱們須要react/vue/angular?使用框架最核心的緣由之一就是提升開發效率,能早點下班。同理,react高階組件可以讓咱們寫出更易於維護的react代碼,能再早點下班~
    • 舉個栗子,ES6支持使用import/export的方式拆分代碼功能和模塊,避免一份文件裏面出現"成坨"的代碼。同理對於複雜的react組件,若是這個組件有幾十個自定義的功能函數,天然要進行拆分,否則又成了"一坨"組件,那麼該如何優雅地拆分組件呢?react高階組件應運而生
    • 在使用ES5編寫react代碼時,可使用Mixin這一傳統模式進行拆分。新版本的react全面支持ES6並提倡使用ES6編寫jsx,同時取消了Mixin。所以高階組件愈來愈受到開源社區的重視,例如redux等知名第三方庫都大量使用了高階組件
  2. 高階組件是什麼?

  • 回答這個問題前,看上圖圖,高階函數就是形如y=kx+b的東西,x是咱們想要改造的原組件,y就是改造事後輸出的組件。那具體是怎麼改造的呢?k和b就是改造的方法。這就是高階組件的基本原理,是否是一點也不高階~
  • 再舉個栗子相信更能讓你明白:咱們寫代碼須要進行加法計算,因而咱們把加法計算的方法單獨抽出來寫成一個加法函數,這個加法函數能夠在各處調用使用,從而減小了工做量和代碼量。而咱們獨立出來的這個能夠隨處使用的加法函數,類比地放在react裏,就是高階組件。
  1. 如何實現高階組件? 從上面的問題回答中,咱們知道了:高階組件其實就是處理react組件的函數。那麼咱們如何實現一個高階組件?有兩種方法:
    • 1.屬性代理
    • 2.反向繼承

高階組件(HOC)是React中用於複用組件邏輯的一種高級技巧。HOC自身不是React API的一部分,他是一種基於React的組合特性而造成的數據模式。vue

不少人看到高階組件(HOC)這個概念就被嚇到了,認爲這東西很難,其實這東西概念真的很簡單,咱們先來看一個例子。react

function add(a, b) {
    return a + b
}
複製代碼

如今若是我想給這個 add 函數添加一個輸出結果的功能,那麼你可能會考慮我直接使用 console.log 不就實現了麼。說的沒錯,可是若是咱們想作的更加優雅而且容易複用和擴展,咱們能夠這樣去作:算法

function add(a, b) {
    return a + b
}
function withLog (fn) {
    function wrapper(a, b) {
        const result = fn(a, b)
        console.log(result)
        return result
    }
    return wrapper
}
const withLogAdd = withLog(add)
withLogAdd(1, 2)
複製代碼

其實這個作法在函數式編程裏稱之爲高階函數,你們都知道 React 的思想中是存在函數式編程的,高階組件高階函數就是同一個東西。咱們實現一個函數,傳入一個組件,而後在函數內部再實現一個函數去擴展傳入的組件,最後返回一個新的組件,這就是高階組件的概念,做用就是爲了更好的複用代碼。編程

具體而言,==高階組件是參數爲組件,返回值爲新組件的函數。==redux

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

組件是將 props 轉換爲 UI,而高階組件是將組件轉換爲另外一個組件。設計模式

接下來,咱們將討論爲何高階組件有用,以及如何編寫本身的 HOC 函數。數組

使用 HOC 解決橫切關注點問題

注意: 咱們以前建議使用 mixins 用於解決橫切關注點相關的問題。但咱們已經意識到 mixins 會產生更多麻煩。閱讀更多 以瞭解咱們爲何要拋棄 mixins 以及如何轉換現有組件。 bash

其實 HOC 和 Vue 中的 mixins 做用是一致的,而且在早期 React 也是使用 mixins 的方式。可是在使用 class 的方式建立組件之後,mixins 的方式就不能使用了,而且其實 mixins 也是存在一些問題的,好比:

  • 隱含了一些依賴,好比我在組件中寫了某個 state 而且在 mixin 中使用了,就這存在了一個依賴關係。萬一下次別人要移除它,就得去 mixin 中查找依賴
  • 多個 mixin 中可能存在相同命名的函數,同時代碼組件中也不能出現相同命名的函數,不然就是重寫了,其實我一直以爲命名真的是一件麻煩事。。
  • 雪球效應,雖然我一個組件仍是使用着同一個 mixin,可是一個 mixin 會被多個組件使用,可能會存在需求使得 mixin 修改本來的函數或者新增更多的函數,這樣可能就會產生一個維護成本

HOC 解決了這些問題,而且它們達成的效果也是一致的,同時也更加的政治正確(畢竟更加函數式了)。app

組件是 React 中代碼複用的基本單元。但你會發現某些模式並不適合傳統組件。框架

例如,假設有一個 CommentList 組件,它訂閱外部數據源,用以渲染評論列表:

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    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>
    );
  }
}
複製代碼

稍後,編寫了一個用於訂閱單個博客帖子的組件,該帖子遵循相似的模式:

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 的模式將一次又一次地發生。咱們須要一個抽象,容許咱們在一個地方定義這個邏輯,並在許多組件之間共享它。這正是高階組件擅長的地方。

對於訂閱了 ++DataSource++ 的組件,好比 ++CommentList++ 和 ++BlogPost++,咱們能夠編寫一個建立組件函數。該函數將接受一個子組件做爲它的其中一個參數,該子組件將訂閱數據做爲 ++prop++。讓咱們調用函數 ++withSubscription++:

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

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

第一個參數是被包裝組件。第二個參數經過 DataSource 和當前的 props 返回咱們須要的數據

當渲染 ++CommentListWithSubscription++ 和 ++BlogPostWithSubscription++ 時, ++CommentList++ 和 ++BlogPost++ 將傳遞一個 ++data prop++,其中包含從 ++DataSource++ 檢索到的最新數據:

// 此函數接收一個組件...
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() {
      // ... 並使用新數據渲染被包裝的組件!
      // 請注意,咱們可能還會傳遞其餘屬性
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}
複製代碼

請注意,HOC 不會修改傳入的組件,也不會使用繼承來複制其行爲。相反,HOC 經過將組件包裝在容器組件中來組成新組件。==HOC 是純函數,沒有反作用==。

被包裝組件接收來自容器組件的全部 prop,同時也接收一個新的用於 render 的 data prop。HOC 不須要關心數據的使用方式或緣由,而被包裝組件也不須要關心數據是怎麼來的。

由於 withSubscription 是一個普通函數,你能夠根據須要對參數進行增添或者刪除。例如,您可能但願使 data prop 的名稱可配置,以進一步將 HOC 與包裝組件隔離開來。或者你能夠接受一個配置 shouldComponentUpdate 的參數,或者一個配置數據源的參數。由於 HOC 能夠控制組件的定義方式,這一切都變得有可能。

與組件同樣,withSubscription 和包裝組件之間的契約徹底基於之間傳遞的 props。這種依賴方式使得替換 HOC 變得容易,只要它們爲包裝的組件提供相同的 prop 便可。例如你須要改用其餘庫來獲取數據的時候,這一點就頗有用。


不要改變原始組件。使用組合。

不要試圖在 HOC 中修改組件原型(或以其餘方式改變它)。

function logProps(InputComponent) {
  InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
    console.log('Current props: ', this.props);
    console.log('Next props: ', nextProps);
  };
  // 返回原始的 input 組件,暗示它已經被修改。
  return InputComponent;
}

// 每次調用 logProps 時,加強組件都會有 log 輸出。
const EnhancedComponent = logProps(InputComponent);
複製代碼

這樣作會產生一些不良後果。其一是==輸入組件再也沒法像 HOC 加強以前那樣使用了==。更嚴重的是,==若是你再用另外一個一樣會修改 componentWillReceiveProps 的 HOC 加強它,那麼前面的 HOC 就會失效!同時,這個 HOC 也沒法應用於沒有生命週期的函數組件。==

修改傳入組件的 HOC 是一種糟糕的抽象方式。調用者必須知道他們是如何實現的,以免與其餘 HOC 發生衝突。

HOC 不該該修改傳入組件,而應該使用組合的方式,經過將組件包裝在容器組件中實現功能:

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }
    render() {
      // 將 input 組件包裝在容器中,而不對其進行修改。Good!
      return <WrappedComponent {...this.props} />;
    }
  }
}
複製代碼

該 HOC 與上文中修改傳入組件的 HOC 功能相同,同時避免了出現衝突的狀況。它一樣適用於 class 組件和函數組件。並且由於它是一個純函數,它能夠與其餘 HOC 組合,甚至能夠與其自身組合。

您可能已經注意到 HOC 與容器組件模式之間有類似之處。容器組件擔任分離將高層和低層關注的責任,由容器管理訂閱和狀態,並將 prop 傳遞給處理渲染 UI。HOC 使用容器做爲其實現的一部分,你能夠將 HOC 視爲參數化容器組件。


約定:將不相關的 props 傳遞給被包裹的組件

HOC 爲組件添加特性。自身不該該大幅改變約定。HOC 返回的組件與原組件應保持相似的接口。

HOC 應該透傳與自身無關的 props。大多數 HOC 都應該包含一個相似於下面的 render 方法:

render() {
  // 過濾掉非此 HOC 額外的 props,且不要進行透傳
  const { extraProp, ...passThroughProps } = this.props;

  // 將 props 注入到被包裝的組件中。
  // 一般爲 state 的值或者實例方法。
  const injectedProp = someStateOrInstanceMethod;

  // 將 props 傳遞給被包裝組件
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}
複製代碼

這種約定保證了 HOC 的靈活性以及可複用性。


約定:最大化可組合性

並非全部的 HOC 都同樣。有時候它僅接受一個參數,也就是被包裹的組件:

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

HOC 一般能夠接收多個參數。好比在 Relay 中,HOC 額外接收了一個配置對象用於指定組件的數據依賴:

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

最多見的 HOC 簽名以下:

// React Redux 的 `connect` 函數
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
複製代碼

剛剛發生了什麼?!若是你把它分開,就會更容易看出發生了什麼。

// connect 是一個函數,它的返回值爲另一個函數。
const enhance = connect(commentListSelector, commentListActions);
// 返回值爲 HOC,它會返回已經鏈接 Redux store 的組件
const ConnectedComment = enhance(CommentList);
複製代碼

換句話說,==connect 是一個返回高階組件的高階函數!==

這種形式可能看起來使人困惑或沒必要要,但它有一個有用的屬性。 像 connect 函數返回的單參數 HOC 具備簽名 Component => Component。 輸出類型與輸入類型相同的函數很容易組合在一塊兒。

// 而不是這樣...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// ... 你能夠編寫組合工具函數
// compose(f, g, h) 等同於 (...args) => f(g(h(...args)))
const enhance = compose(
  // 這些都是單參數的 HOC
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
複製代碼

(一樣的屬性也容許 connect 和其餘 HOC 承擔裝飾器的角色,裝飾器是一個實驗性的 JavaScript 提案。)

許多第三方庫都提供了 compose 工具函數,包括 lodash (好比 lodash.flowRight), Redux 和 Ramda。


約定:包裝顯示名稱以便輕鬆調試

HOC 建立的容器組件會與任何其餘組件同樣,會顯示在 React Developer Tools 中。爲了方便調試,請選擇一個顯示名稱,以代表它是 HOC 的產物。

最多見的方式是用 HOC 包住被包裝組件的顯示名稱。好比高階組件名爲 withSubscription,而且被包裝組件的顯示名稱爲 CommentList,顯示名稱應該爲 ==WithSubscription(CommentList)==:

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

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

注意事項

高階組件有一些須要注意的地方,對於 React 新手來講可能並不容易發現。

不要在 render 方法中使用 HOC

React 的 diff 算法(稱爲協調)使用組件標識來肯定它是應該更新現有子樹仍是將其丟棄並掛載新子樹。 若是從 render 返回的組件與前一個渲染中的組件相同(===),則 React 經過將子樹與新子樹進行區分來遞歸更新子樹。 若是它們不相等,則徹底卸載前一個子樹。

一般,你不須要考慮這點。但對 HOC 來講這一點很重要,由於這表明着你不該在組件的 render 方法中對一個組件應用 HOC:

render() {
  // 每次調用 render 函數都會建立一個新的 EnhancedComponent
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // 這將致使子樹每次渲染都會進行卸載,和從新掛載的操做!
  return <EnhancedComponent />;
}
複製代碼

這不只僅是性能問題 - 從新掛載組件會致使該組件及其全部子組件的狀態丟失。

若是在組件以外建立 HOC,這樣一來組件只會建立一次。所以,每次 render 時都會是同一個組件。通常來講,這跟你的預期表現是一致的。

在極少數狀況下,你須要動態調用 HOC。你能夠在組件的生命週期方法或其構造函數中進行調用。

務必複製靜態方法

有時在 React 組件上定義靜態方法頗有用。例如,Relay 容器暴露了一個靜態方法 getFragment 以方便組合 GraphQL 片斷。

可是,當你將 HOC 應用於組件時,原始組件將使用容器組件進行包裝。這意味着新組件沒有原始組件的任何靜態方法。

// 定義靜態函數
WrappedComponent.staticMethod = function() {/*...*/}
// 如今使用 HOC
const EnhancedComponent = enhance(WrappedComponent);

// 加強組件沒有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
複製代碼

爲了解決這個問題,你能夠在返回以前把這些方法拷貝到容器組件上:

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // 必須準確知道應該拷貝哪些方法 :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}
複製代碼

但要這樣作,你須要知道哪些方法應該被拷貝。你可使用 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;
}
複製代碼

除了導出組件,另外一個可行的方案是再額外導出這個靜態方法。

// 使用這種方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...單獨導出該方法...
export { someFunction };

// ...並在要使用的組件中,import 它們
import MyComponent, { someFunction } from './MyComponent.js';
複製代碼

Refs 不會被傳遞

雖然高階組件的約定是將全部 props 傳遞給被包裝組件,但這對於 refs 並不適用。那是由於 ref 實際上並非一個 prop - 就像 key 同樣,它是由 React 專門處理的。若是將 ref 添加到 HOC 的返回組件中,則 ref 引用指向容器組件,而不是被包裝組件。

這個問題的解決方案是經過使用 React.forwardRef API(React 16.3 中引入)。前往 ref 轉發章節瞭解更多。


HOC的實際應用

日誌打點

可用、權限控制

雙向綁定

在vue中,綁定一個變量後可實現雙向數據綁定,即表單中的值改變後綁定的變量也會自動改變。而React中沒有作這樣的處理,在默認狀況下,表單元素都是非受控組件。給表單元素綁定一個狀態後,每每須要手動書寫onChange方法來將其改寫爲受控組件,在表單元素很是多的狀況下這些重複操做是很是痛苦的。 咱們能夠藉助高階組件來實現一個簡單的雙向綁定,代碼略長,能夠結合下面的思惟導圖進行理解。

表單校驗


總結

高階組件是屬於 React 高級運用,可是實際上是一個很簡單的概念,可是它很是實用。在實際的業務場景中,靈活合理的使用高階組件,能夠提升代碼的複用性和靈活性。

對高階組件,咱們能夠總結如下幾點:

  • 高階組件是一個函數,而不是組件
  • 組件是把 props 轉化成 UI,高階組件是把一個組件轉化成另外一個組件
  • 高階組件的做用是複用代碼
  • 高階組件對應設計模式裏的==裝飾者模式==

相關文章
相關標籤/搜索