深刻淺出理解 React高階組件

開始以前,有兩點須要說明一下:一、React 高階組件 僅僅是一種模式,並非 React 的基礎知識;二、它不是開發 React app 的必要知識。你能夠略過此文章,仍然能夠開發 React app。然而,技多不壓身,若是你也是一位 React 開發者,強烈建議你掌握它。javascript

1、爲何須要高階組件

若是你不知道 Don't Repeat YourselfD.R.Y,那麼在軟件開發中一定走不太遠。對於大多數開發者來講,它是一個開發準則。在這篇文章當中,咱們將瞭解到如何在 React 當中運用 DRY 原則 —— 高階組件。開始闡述以前,咱們先來認識一下問題所在。java

假設咱們要開發相似下圖的功能。正如大多的項目同樣,咱們先按流程開發着。當開發到差很少的時候,你會發現頁面上有不少,鼠標懸浮在某個元素上出現 tooltip 的場景。react

圖片

有不少種方法作到這樣。你可能想到寫一個帶懸浮狀態的組件來控制 tooltip 的顯示與否。那麼你須要添加三個組件——Info, TrendChart 和 DailyChart。編程

咱們從 Info 組件開始。它很簡單,僅僅是一個 SVG icon.數組

class Info extends React.Component {
  render() {
    return (
      <svg className="Icon-svg Icon--hoverable-svg" height={this.props.height} viewBox="0 0 16 16" width="16" > <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" /> </svg> ); } } 複製代碼

而後咱們須要添加一個狀態來記錄組件是否被 Hover,能夠用 React 鼠標事件當中的 onMouseOveronMouseOut來實現。app

class Info extends React.Component {
  state = { hovering: false };
  mouseOver = () => this.setState({ hovering: true });
  mouseOut = () => this.setState({ hovering: false });
  render() {
    return (
      <>
        {this.state.hovering === true ? <Tooltip id={this.props.id} /> : null}
        <svg
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
          className="Icon-svg Icon--hoverable-svg"
          height={this.props.height}
          viewBox="0 0 16 16"
          width="16"
        >
          <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
        </svg>
      </>
    );
  }
}
複製代碼

看起來還不錯,咱們須要在 TrendChartDailyChart寫一樣的邏輯。svg

class TrendChart extends React.Component {
  state = { hovering: false };
  mouseOver = () => this.setState({ hovering: true });
  mouseOut = () => this.setState({ hovering: false });
  render() {
    return (
      <>
        {this.state.hovering === true ? <Tooltip id={this.props.id} /> : null}
        <Chart
          type="trend"
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
        />
      </>
    );
  }
}
複製代碼
class DailyChart extends React.Component {
  state = { hovering: false };
  mouseOver = () => this.setState({ hovering: true });
  mouseOut = () => this.setState({ hovering: false });
  render() {
    return (
      <>
        {this.state.hovering === true ? <Tooltip id={this.props.id} /> : null}
        <Chart
          type="daily"
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
        />
      </>
    );
  }
}
複製代碼

三個組件咱們都開發完成。但正如你看到的,很是不 DRY ,由於咱們在三個組件中把同一套 hover 邏輯 重複了三次。函數

問題就顯而易見了。當一個新組件須要相似 hover 邏輯 時,咱們應避免重複。那麼,咱們該如何解決呢?爲了便於理解,先來了解一下編程當中的兩個概念—— 回調高階函數學習

2、什麼是回調和高階函數

在 JavaScript 當中,函數是第一公民。也就是說它能夠像 objects/arrays/strings 被賦值給變量、被看成參數傳遞給函數和被函數返回。ui

function add(x, y) {
  return x + y;
}

function addFive(x, addReference) {
  return addReference(x, 5);
}

addFive(10, add); // 15
複製代碼

你可能會感到有點兒繞:咱們在 函數addFive 中傳入一個函數名爲 addReference 的參數,而且在內部返回時調用它。相似這種狀況,你把它看成參數傳遞的函數叫 回調;接收函數做爲參數的函數叫 高階函數

爲了更直觀,咱們把上述代碼的命名概念化。

function add(x, y) {
  return x + y;
}

function higherOrderFunction(x, callback) {
  return callback(x, 5);
}

higherOrderFunction(10, add);
複製代碼

這種寫法其實很常見。若是你用過數組方法、jQuery 或 lodash 庫,那麼你就使用過 回調 和 高階函數。

[1, 2, 3].map(i => i + 5);

_.filter([1, 2, 3, 4], n => n % 2 === 0);

$("#btn").on("click", () => console.log("Callbacks are everywhere"));
複製代碼

3、高階函數的簡單應用

回到以前寫的那個例子。咱們不只須要 addFive,可能還需 addTen addTwenty等等。依照如今的寫法,當咱們寫一個新函數的時候,不得不重複原有邏輯。

function add(x, y) {
  return x + y;
}

function addFive(x, addReference) {
  return addReference(x, 5);
}

function addTen(x, addReference) {
  return addReference(x, 10);
}

function addTwenty(x, addReference) {
  return addReference(x, 20);
}

addFive(10, add); // 15
addTen(10, add); // 20
addTwenty(10, add); // 30
複製代碼

看起來還不錯,但仍然有點重複。咱們的目的是用更少的代碼建立更多的 adder函數(addFive, addTen, addTwenty 等等)。鑑於此,咱們建立一個makeAdder函數 ,此函數接收一個 數字 和 一個函數 做爲參數,長話少說,直接看代碼。

function add(x, y) {
  return x + y;
}

function makeAdder(x, addReference) {
  return function(y) {
    return addReference(x, y);
  };
}

const addFive = makeAdder(5, add);
const addTen = makeAdder(10, add);
const addTwenty = makeAdder(20, add);

addFive(10); // 15
addTen(10); // 20
addTwenty(10); // 30
複製代碼

很好,如今咱們想要多少 adder函數 就能寫多少,而且不必寫那麼多重複代碼。

這種使用一個函數並將其應用一個或多個參數,但不是所有參數,在這個過程當中建立並返回一個新函數叫『偏函數應用』。 JavaScript 當中的 .bind即是這種方法的一個例子。

4、高階組件

那麼,這些和咱們最初寫 React 代碼重複又有什麼關係呢?也像建立 高階函數makeAdder 同樣地建立相似 高階組件 。看起來還不錯,咱們試試吧。

高階函數

  • 一個函數
  • 接收一個回調函數爲參數
  • 返回一個新的函數
  • 返回的函數能夠調用傳進去的回調函數
function higherOrderFunction(callback) {
  return function() {
    return callback();
  };
}
複製代碼

高階組件

  • 一個組件
  • 接收一個組件爲參數
  • 返回一個新的組件
  • 返回的組件能夠渲染當初傳進去的組件
function higherOrderComponent(Component) {
  return class extends React.Component {
    render() {
      return <Component />; } }; } 複製代碼

5、高階組件的簡單應用

好,咱們如今理解了高階組件的基本概念。你應該還記得,最初面臨的問題是在太多地方重複了 Hover 邏輯 部分。

state = { hovering: false };
mouseOver = () => this.setState({ hovering: true });
mouseOut = () => this.setState({ hovering: false });
複製代碼

記住,咱們但願高階組件(命名爲 withHover)能壓縮 Hover 邏輯 部分,並帶有 hovering 狀態,這樣能避免咱們重複 Hover 邏輯。

最終目標,不管什麼時候咱們想寫一個帶 Hover 狀態的組件時,均可以把這個組件做爲參數傳入咱們的高階組件 withHover

const InfoWithHover = withHover(Info);
const TrendChartWithHover = withHover(TrendChart);
const DailyChartWithHover = withHover(DailyChart);
複製代碼

接着,不管什麼組件傳入 withHover ,都會返回組件自己,而且會接收一個 hovering 屬性。

function Info({ hovering, height }) {
  return (
    <>
      {hovering === true ? <Tooltip id={this.props.id} /> : null}
      <svg
        className="Icon-svg Icon--hoverable-svg"
        height={height}
        viewBox="0 0 16 16"
        width="16"
      >
        <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
      </svg>
    </>
  );
}
複製代碼

如今,咱們須要開始寫 withHover組件 了。正如以上,須要作到如下三點:

  • 接收一個『組件』爲參數
  • 返回一個新的組件
  • 參數組件接收一個 「hovering」 屬性

一、接收一個『組件』爲參數

function withHover(Component) {}
複製代碼

二、返回一個新的組件

function withHover(Component) {
  return class WithHover extends React.Component {};
}
複製代碼

三、參數組件接收一個 「hovering」 屬性

新問題來了, hovering 該從哪裏來?咱們能夠建立一個新的組件,把 hovering 看成該組件的狀態,而後傳給最初的那個參數組件。

function withHover(Component) {
  return class WithHover extends React.Component {
    state = { hovering: false };
    mouseOver = () => this.setState({ hovering: true });
    mouseOut = () => this.setState({ hovering: false });
    render() {
      return (
        <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}> <Component hovering={this.state.hovering} /> </div> ); } }; } 複製代碼

我想起了一句話:組件是把 props 轉換成 UI 的過程;高階組件是把一個組件轉換成另外一個組件的過程。

咱們已經學習完了高階函數的基礎知識,但仍然有幾點值得討論。

6、高階組件的進階應用

回頭看看組件 withHover ,仍是有一點不足:就是它假想了用戶傳進去的參數組件必需要接收一個名爲 hovering 的 prop;若是參數組件自己就有一個名爲 hovering 的 prop,而且這個 prop 並非來處理 hover 的, 就會形成命名衝突。咱們能夠嘗試一下讓用戶自定義控制 hover 的 prop 命名。

function withHover(Component, propName = "hovering") {
  return class WithHover extends React.Component {
    state = { hovering: false };
    mouseOver = () => this.setState({ hovering: true });
    mouseOut = () => this.setState({ hovering: false });
    render() {
      const props = {
        [propName]: this.state.hovering
      };

      return (
        <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}> <Component {...props} /> </div> ); } }; } 複製代碼

在 withHover 中,咱們給 propName 設定了一個默認值 hovering,用戶也能夠在組件中傳入第二個參數自定義命名。

function withHover(Component, propName = "hovering") {
  return class WithHover extends React.Component {
    state = { hovering: false };
    mouseOver = () => this.setState({ hovering: true });
    mouseOut = () => this.setState({ hovering: false });
    render() {
      const props = {
        [propName]: this.state.hovering
      };

      return (
        <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>
          <Component {...props} />
        </div>
      );
    }
  };
}

function Info({ showTooltip, height }) {
  return (
    <>
      {showTooltip === true ? <Tooltip id={this.props.id} /> : null}
      <svg
        className="Icon-svg Icon--hoverable-svg"
        height={height}
        viewBox="0 0 16 16"
        width="16"
      >
        <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
      </svg>
    </>
  );
}

const InfoWithHover = withHover(Info, "showTooltip");
複製代碼

你可能又注意到了另一個問題,在組件 Info 中,它還接收一個名爲 height 的 prop。按照如今這種寫法,height 只能是 undefined,但咱們指望能達到以下效果:

const InfoWithHover = withHover(Info)

...

return <InfoWithHover height="16px" />
複製代碼

咱們把 height 傳入 InfoWithHover ,可是該如何使它生效呢?

function withHover(Component, propName = "hovering") {
  return class WithHover extends React.Component {
    state = { hovering: false };
    mouseOver = () => this.setState({ hovering: true });
    mouseOut = () => this.setState({ hovering: false });
    render() {
      console.log(this.props); // { height: "16px" }

      const props = {
        [propName]: this.state.hovering
      };

      return (
        <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}> <Component {...props} /> </div> ); } }; } 複製代碼

從 console 中能夠看出, this.props 的值是 { height: "16px" } 。咱們要作的就是無論 this.props 爲什麼值,都把 它傳給參數組件 Component

render() {
      const props = {
        [propName]: this.state.hovering,
        ...this.props,
      }

      return (
        <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}> <Component {...props} /> </div> ); } 複製代碼

最終,咱們能夠看出,經過使用高階組件能夠有效地複用同套邏輯,避免過多的重複代碼。可是,它真的沒有任何缺點嗎?顯然不是。

7、高階組件的小瑕疵

當咱們使用高階組件的時候,可能會發生 inversion of control(控制反轉) 。想象一下,假如咱們正使用 React Router 的 withRouter ,根據文檔:不管是什麼組件,它都會把 match, locationhistory 傳給該組件的 prop。

class Game extends React.Component {
  render() {
    const { match, location, history } = this.props // From React Router

    ...
  }
}

export default withRouter(Game)
複製代碼

從上能夠看出,若是咱們的組件 Game 也有命名爲 match, locationhistory 的 prop 時,便會引起命名衝突。這個問題,咱們在寫組件 withHover 遇到過,並經過傳入第二參數自定義命名的方式解決了該問題。可是當咱們用到第三方庫中的高階組件時,就不必定會有那麼幸運了。咱們不得不修改咱們自身組件 prop 的命名 或 中止使用第三方庫中的該高階組件。

8、結尾

本文是翻譯自 [React Higher-Order Components](React Higher-Order Components),僅供學習參考。若是給您學習理解形成了迷惑,歡迎聯繫我。

相關文章
相關標籤/搜索