深入理解 React 高階元件

發表於2018-11-29

深入理解 React 高階元件

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

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

假設我們要負責重新建立一個類似於 Sprite(譯者注:國外的一個線上支付公司)的儀表盤。正如大多數專案那樣,一切事務在最後收尾之前都工作得很正常。你發現在儀表盤上有一串不一樣的提示框需要你某些元素 hover 的時候顯示。 => 你在儀表盤上面發現了一些不同的、(當滑鼠)懸停在某些組成元素上面會出現的提示資訊。

深入理解 React 高階元件

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

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

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

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

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

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

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

在 JavaScript 中,函式是 “一等公民”。這意味著它就像物件/陣列/字串那樣可以被宣告為一個變數、當作函式的引數或者在函式中返回一個函式,即使返回的是其他函式也可以。

如果你沒這樣用過,你可能會感到困惑。我們將 add 函式作為一個引數傳入 addFive 函式,重新命名為 addReference,然後我們呼叫了著個函式。

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

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

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

讓我們回到我們之前的例子。如果我們不僅僅想建立一個 addFive 函式,我們也想建立 addTen函式、 addTwenty 函式等等,我們該怎麼辦?在我們當前的實踐方法中,我們必須在需要的時候去重複地寫我們的邏輯。

再一次出現這種情況,這樣寫並不糟糕,但是我們重複寫了好多相似的邏輯。這裡我們的目標是要能根據需要寫很多 “adder” 函式(addFiveaddTenaddTwenty 等等),同時儘可能減少程式碼重複。為了完成這個目標,我們建立一個 makeAdder 函式怎麼樣?著個函式可以傳入一個數字和原始 add 函式。因為這個函式的目的是建立一個新的 adder 函式,我們可以讓其返回一個全新的傳遞數字來實現加法的函式。這兒講的有點多,讓我們來看下程式碼吧。

太酷了!現在我們可以在需要的時候隨意地用最低的程式碼重複度建立 “adder” 函式。

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

好吧,那這與 React 以及我們之前遇到滑鼠懸停的元件有什麼關係呢?我們剛剛通過建立了我們的 makeAdder 這個高階函式來實現了程式碼複用,那我們也可以建立一個類似的 “高階元件” 來幫助我們實現相同的功能(程式碼複用)。不過,不像高階函式返回一個新的函式那樣,高階元件返回一個新的元件來渲染 “回撥” 元件。這裡有點複雜,讓我們來攻克它。

(我們的)高階函式
  • 是一個函式
  • 有一個回撥函式做為引數
  • 返回一個新的函式
  • 返回的函式會觸發我們之前傳入的回撥函式

(我們的)高階元件
  • 是一個元件
  • 有一個元件做為引數
  • 返回一個新的元件
  • 返回的元件會渲染我們之前傳入的元件

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

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

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

於是,無論給 withHover 傳遞什麼元件,它都會渲染原始元件,同時傳遞一個 hovering prop。

現在我們需要做的最後一件事是實現 withHover。正如我們上面所看到的:

  • 傳入一個元件引數
  • 返回一個新的元件
  • 渲染傳入引數的那個元件同時注入一個 “hovering” prop。
傳入一個元件引數

返回一個新的元件

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

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

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


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

如果你再回去看我們的 withHover 高階元件的話,它有一個缺點就是它已經假定了一個名為 hovering 的 prop。在大多數情況下這樣或許是沒問題的,但是在某些情況下會出問題。舉個例子,如果(原來的)元件已經有一個叫做 hovering 的 prop 呢?這裡我們出現了命名衝突。我們可以做的是讓我們的 withHover 高階元件能夠允許使用者自己定義傳入子元件的 prop 名。因為 withHover 只是一個函式,讓我們讓它的第二個引數來描述傳遞給子元件 prop 的名字。

現在我們設定了預設的 prop 名稱為 hovering(通過使用 ES6 的預設引數特性來實現),如果使用者想改變 withHover 的預設 prop 名的話,可以通過第二個引數來傳遞一個新的 prop 名。


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

height prop 通過 InfoWithHover 元件傳入,但是這個元件是從哪兒來的?它是我們通過 withHover 所建立並返回的那個元件。

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


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

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

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

相關文章