開始以前,有兩點須要說明一下:一、React 高階組件 僅僅是一種模式,並非 React 的基礎知識;二、它不是開發 React app 的必要知識。你能夠略過此文章,仍然能夠開發 React app。然而,技多不壓身,若是你也是一位 React 開發者,強烈建議你掌握它。javascript
若是你不知道 Don't Repeat Yourself
或 D.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 鼠標事件當中的 onMouseOver
和 onMouseOut
來實現。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>
</>
);
}
}
複製代碼
看起來還不錯,咱們須要在 TrendChart
和 DailyChart
寫一樣的邏輯。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 邏輯 時,咱們應避免重複。那麼,咱們該如何解決呢?爲了便於理解,先來了解一下編程當中的兩個概念—— 回調 和 高階函數。學習
在 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"));
複製代碼
回到以前寫的那個例子。咱們不只須要 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
即是這種方法的一個例子。
那麼,這些和咱們最初寫 React 代碼重複又有什麼關係呢?也像建立 高階函數makeAdder
同樣地建立相似 高階組件
。看起來還不錯,咱們試試吧。
高階函數
function higherOrderFunction(callback) {
return function() {
return callback();
};
}
複製代碼
高階組件
function higherOrderComponent(Component) {
return class extends React.Component {
render() {
return <Component />; } }; } 複製代碼
好,咱們如今理解了高階組件的基本概念。你應該還記得,最初面臨的問題是在太多地方重複了 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組件
了。正如以上,須要作到如下三點:
一、接收一個『組件』爲參數
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 的過程;高階組件是把一個組件轉換成另外一個組件的過程。
咱們已經學習完了高階函數的基礎知識,但仍然有幾點值得討論。
回頭看看組件 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> ); } 複製代碼
最終,咱們能夠看出,經過使用高階組件能夠有效地複用同套邏輯,避免過多的重複代碼。可是,它真的沒有任何缺點嗎?顯然不是。
當咱們使用高階組件的時候,可能會發生 inversion of control(控制反轉)
。想象一下,假如咱們正使用 React Router 的 withRouter
,根據文檔:不管是什麼組件,它都會把 match
, location
和history
傳給該組件的 prop。
class Game extends React.Component {
render() {
const { match, location, history } = this.props // From React Router
...
}
}
export default withRouter(Game)
複製代碼
從上能夠看出,若是咱們的組件 Game
也有命名爲 match
, location
和history
的 prop 時,便會引起命名衝突。這個問題,咱們在寫組件 withHover
遇到過,並經過傳入第二參數自定義命名的方式解決了該問題。可是當咱們用到第三方庫中的高階組件時,就不必定會有那麼幸運了。咱們不得不修改咱們自身組件 prop 的命名 或 中止使用第三方庫中的該高階組件。
本文是翻譯自 [React Higher-Order Components](React Higher-Order Components),僅供學習參考。若是給您學習理解形成了迷惑,歡迎聯繫我。