深入理解 React 高階組件


深入理解 React 高階組件

在這篇文章的開始之前,我們有兩點需要注意:首先,我們所討論的僅僅是一種設計模式。它甚至就像組件結構一樣不是 React 里的東西。第二,它不是構建一個 React 應用所必須的知識。你可以關掉這篇文章、不學習在這篇文章中我們所討論的內容,之后仍然可以構建一個正常的 React 應用。不過,就像構建所有東西一樣,你有更多可用的工具就會得到更好的結果。如果你在寫 React 應用,在你的“工具箱”之中沒有這個(React 高階組件)的話會對你是非常不利的。

在你聽到 Don't Repeat Yourself 或者 D.R.Y 這樣(中邪一樣)的口號之前你是不會在軟件開發的鑽研之路上走得很遠的。有時候實行這些名言會有點過於麻煩,但是在大多數情況下,(實行它)是一個有價值的目標。在這篇文章中我們將會去探討在 React 庫中實現 DRY 的最著名的模式——高階組件。不過在我們探索答案之前,我們首先必須要完全明確問題來源。

假設我們要負責重新創建一個類似於 Sprite(譯者注:國外的一個在線支付公司)的儀表盤。正如大多數項目那樣,一切事務在最后收尾之前都工作得很正常。你在儀表盤上面發現了一些不同的、(當鼠標)懸停在某些組成元素上面會出現的提示信息。

這里有好幾種方式可以實現這個效果。其中一個你可能想到的是監聽特定的組件的 hover 狀態來決定是否展示 tooltip。在上圖中,你有三個組件需要添加它們的監聽功能 —— InfoTrendChart 和 DailyChart

讓我們從 Info 組件開始。現在它只是一個簡單的 SVG 圖標。

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>
    )
  }
}

 

現在我們需要添加讓它可以監測到自身是否被(鼠標)懸停的功能。我們可以使用 React 所附帶的 onMouseOver 和 onMouseOut 這兩個鼠標時間。我們傳遞給 onMouseOver 的函數將會在組件被鼠標懸停后觸發,同時我們傳遞給 onMouseOut 的函數將會在組件不再被鼠標懸停時觸發。要以 React 的方式來操作,我們會給給我們的組件添加一個 hovering state 屬性,所以我們可以在 hovering state 屬性改變的時候觸發重繪,來展示或者隱藏我們的提示框。

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>
      </>
    )
  }
}

 

上面的代碼看起來很棒。現在我們要添加同樣的功能給我們的其他兩個組件 —— TrendChart 和 DailyChart。如果這兩個組件沒有出問題,就請不要修復它。我們對於 Info 的懸停功能運行的很好,所以請再寫一遍之前的代碼。

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}
        />
      </>
    )
  }
}

 

你或許知道下一步了:我們要對最后一個組件 DailyChart 做同樣的事情。

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}
        />
      </>
    )
  }
}

 

這樣的話,我們就全部做完了。你可能以前曾經這樣寫過 React 代碼。但這並不該是你最終所該做的(不過這樣做也還湊合),但是它很不 “DRY”。正如我們所看到的,我們在我們的每一個組件中都 重復着完全一樣的的鼠標懸停邏輯。

從這點看的話,問題變得非常清晰了:我們希望避免在在每個需要添加鼠標懸停邏輯的組件是都再寫一遍相同的邏輯。所以,解決辦法是什么?在我們開始前,讓我們先討論一些能讓我們更容易理解答案的編程思想 —— 回調函數 和 高階函數

在 JavaScript 中,函數是“一等公民”。這意味着它就像對象/數組/字符串那樣可以被聲明為一個變量、當作函數的參數或者在函數中返回一個函數,即使返回的是其他函數也可以。

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

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

addFive(10, add) // 15

 

如果你沒這樣用過,你可能會感到困惑。我們將 add 函數作為一個參數傳入 addFive 函數,重新命名為 addReference,然后我們調用了着個函數。

這時候,你作為參數所傳遞進去的函數被叫做回調函數同時你使用回調函數所構建的新函數被叫做高階函數。

因為這些名詞很重要,下面是一份根據它們所表示的含義重新命名變量后的同樣邏輯的代碼。

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

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

higherOrderFunction(10, add)

 

這個模式很常見,哪里都有它。如果你之前用過任何 JavaScript 數組方法、jQuery 或者是 lodash 這類的庫,你就已經用過高階函數和回調函數了。

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

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

$('#btn').on('click', () =>
  console.log('回調函數哪里都有')
)

 

讓我們回到我們之前的例子。如果我們不僅僅想創建一個 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” 函數(addFiveaddTenaddTwenty 等等),同時盡可能減少代碼重復。為了完成這個目標,我們創建一個 makeAdder 函數怎么樣?着個函數可以傳入一個數字和原始 add 函數。因為這個函數的目的是創建一個新的 adder 函數,我們可以讓其返回一個全新的傳遞數字來實現加法的函數。這兒講的有點多,讓我們來看下代碼吧。

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” 函數。

如果你在意的話,這個通過一個多參數的函數來返回一個具有較少參數的函數的模式被叫做“部分應用(Partial Application)”,它也是函數式編程的技術。JavaScript 內置的 “.bind” 方法也是一個類似的例子。

好吧,那這與 React 以及我們之前遇到鼠標懸停的組件有什么關系呢?我們剛剛通過創建了我們的 makeAdder 這個高階函數來實現了代碼復用,那我們也可以創建一個類似的“高階組件”來幫助我們實現相同的功能(代碼復用)。不過,不像高階函數返回一個新的函數那樣,高階組件返回一個新的組件來渲染“回調”組件 🤯。這里有點復雜,讓我們來攻克它。

(我們的)高階函數

  • 是一個函數
  • 有一個回調函數做為參數
  • 返回一個新的函數
  • 返回的函數會觸發我們之前傳入的回調函數
function higherOrderFunction (callback) {
  return function () {
    return callback()
  }
}

 

(我們的)高階組件

  • 是一個組件
  • 有一個組件做為參數
  • 返回一個新的組件
  • 返回的組件會渲染我們之前傳入的組件
function higherOrderComponent (Component) {
  return class extends React.Component {
    render() {
      return <Component />
    }
  }
}

 

我們已經有了一個高階函數的基本概念了,現在讓我們來完善它。如果你還記得的話,我們之前的問題是我們重復地在每個需要的組件上寫我們的鼠標懸停的處理邏輯。

state = { hovering: false }
mouseOver = () => this.setState({ hovering: true })
mouseOut = () => this.setState({ hovering: false })

 

考慮到這一點,我們希望我們的高階組件(我們把它稱作 withHover)自身需要能封裝我們的鼠標懸停處理邏輯然后傳遞 hovering state 給其所需要渲染的組件。這將允許我們能夠復用鼠標懸停邏輯,並將其裝入單一的位置(withHover)。

最后,下面的代碼就是我們的最終目標。無論什么時候我們想讓一個組件具有 hovering state,我們都可以通過將它傳遞給 withHover 高階組件來實現。

const InfoWithHover = withHover(Info)
const TrendChartWithHover = withHover(TrendChart)
const DailyChartWithHover = withHover(DailyChart)

 

於是,無論給 withHover 傳遞什么組件,它都會渲染原始組件,同時傳遞一個 hoveringprop。

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” prop。

傳入一個組件參數

function withHover (Component) {

}

 

返回一個新的組件

function withHover (Component) {
  return class WithHover extends React.Component {

  }
}

 

渲染傳入參數的那個組件同時注入一個 “hovering” prop

現在問題變為了我們應該如何獲取 hovering 呢?好吧,我們已經有之前寫邏輯的代碼了。我們僅僅需要將其添加到一個新的組件同時將 hovering state 作為一個 prop 傳遞給參數中的 組件

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>
      );
    }
  }
}

 

我比較喜歡的思考這些知識的方式(同時也在 React 文檔中有提到)是 **組件是將 props 轉化到視圖層,高階組件則是將一個組件轉化到另一個組件。**在我們的例子中,我們將我們的 InfoTrendChart 和 DailyChart 組件搬運到一個具有 hovering prop 的組件中。


至此,我們已經涵蓋到了高階組件的所有基礎知識。這里還有一些很重要的知識我們需要來說明下。

如果你再回去看我們的 withHover 高階組件的話,它有一個缺點就是它已經假定了一個名為 hovering 的 prop。在大多數情況下這樣或許是沒問題的,但是在某些情況下會出問題。舉個例子,如果(原來的)組件已經有一個叫做 hovering 的 prop 呢?這里我們出現了命名沖突。我們可以做的是讓我們的 withHover 高階組件能夠允許用戶自己定義傳入子組件的 prop 名。因為 withHover 只是一個函數,讓我們讓它的第二個參數來描述傳遞給子組件 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>
      );
    }
  }
}

 

現在我們設置了默認的 prop 名稱為 hovering(通過使用 ES6 的默認參數特性來實現),如果用戶想改變 withHover 的默認 prop 名的話,可以通過第二個參數來傳遞一個新的 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>
      );
    }
  }
}

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')

 


你可能發現了我們的 withHover 函數實現的另外一個問題。看看我們的 Info 組件,你可能會發現其還有一個 height 屬性,但是 height將會是 undefined。其原因是我們的 withHover組件是渲染 Component 組件的函數。事實上我們這樣做的話,除了 hovering prop 以外我們不會傳遞任何 prop 給我們最終創建的 <Component />

const InfoWithHover = withHover(Info)

...

return <InfoWithHover height="16px" />
height prop 通過 InfoWithHover 組件傳入,但是這個組件是從哪兒來的?它是我們通過 withHover 所創建並返回的那個組件。

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>
      );
    }
  }
}

 

深入 WithHover 組件內部,this.props.height 的值是 16px 但是我們沒有用它做任何事情。我們需要確保我們將其傳入給我們實際渲染的 Component

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

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

 


由此來看,我們已經感受到了使用高階組件減少代碼重復的諸多優點。但是,它(高階組件)還有什么坑嗎?當然有,我們馬上就去踩踩這些坑。

當我們使用高階組件時,會發生一些控制反轉的情況。想象下我們正在用類似於 React Router 的 withRouter 這類第三方的高階組件。根據它們的文檔,“withRouter 將會在任何被它包裹的組件渲染時,將 matchlocation 和 history prop 傳遞給它們”。

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

    ...
  }
}

export default withRouter(Game)

 

請注意,我們並沒有(由 <Game /> 組件直接)在界面上渲染 Game 元素。我們將我們的組件全權交給了 React Router 同時我們也相信其不止能正確渲染組件,也能正確傳遞 props。我們之前在討論 hovering prop 命名沖突的時候看到過這個問題。為了修復這個問題我們嘗試着給我們的 withHover 高階組件傳遞第二個參數來允許修改 prop 的名字。但是在使用第三方高階組件的時候,我們沒有這個配置項。如果我們的 Game 組件已經使用了 matchlocation 或者 history 的話,就沒有(像使用我們自己的組件)那沒幸運了。我們除了改變我們之前所需要使用的 props 名之外就只能不使用 withRouter 高階組件了。

有人說道 : 這個通過一個多參數的函數來返回一個具有較少參數的函數的模式被叫做“部分應用(Partial Application)” 叫偏函數


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM