要問程序員同學們尤為著名的模式是什么,當然是在React 庫實現(xiàn) DRY 高階組件,進階文章來了,要準備好哦~
在你聽到 Don't Repeat Yourself或者 D.R.Y 這樣(中邪一樣)的口號之前你是不會在軟件開發(fā)的鉆研之路上走得很遠的。有時候實行這些名言會有點過于麻煩,但是在大多數(shù)情況下,(實行它)是一個有價值的目標。在這篇文章中我們將會去探討在 React 庫中實現(xiàn) DRY 的尤為著名的模式——高階組件。不過在我們探索答案之前,我們首先必須要完全明確問題來源。
假設我們要負責重新創(chuàng)建一個類似于 Sprite(譯者注:國外的一個在線支付公司)的儀表盤。正如大多數(shù)項目那樣,一切事務在最后收尾之前都工作得很正常。你發(fā)現(xiàn)在儀表盤上有一串不一樣的提示框需要你某些元素 hover 的時候顯示。 => 你在儀表盤上面發(fā)現(xiàn)了一些不同的、(當鼠標)懸停在某些組成元素上面會出現(xiàn)的提示信息。
這里有好幾種方式可以實現(xiàn)這個效果。其中一個你可能想到的是監(jiān)聽特定的組件的 hover 狀態(tài)來決定是否展示 tooltip。在上圖中,你有三個組件需要添加它們的監(jiān)聽功能—— Info、TrendChart 和 DailyChart。
讓我們從 Info 組件開始?,F(xiàn)在它只是一個簡單的 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> ) }}復制代碼現(xiàn)在我們需要添加讓它可以監(jiān)測到自身是否被(鼠標)懸停的功能。我們可以使用 React 所附帶的 onMouseOver 和 onMouseOut 這兩個鼠標時間。我們傳遞給 onMouseOver 的函數(shù)將會在組件被鼠標懸停后觸發(fā),同時我們傳遞給 onMouseOut 的函數(shù)將會在組件不再被鼠標懸停時觸發(fā)。要以 React 的方式來操作,我們會給給我們的組件添加一個 hovering state 屬性,所以我們可以在 hovering state 屬性改變的時候觸發(fā)重繪,來展示或者隱藏我們的提示框。
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> </> ) }}復制代碼上面的代碼看起來很棒?,F(xiàn)在我們要添加同樣的功能給我們的其他兩個組件——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} /> </> ) }}復制代碼這樣的話,我們就全部做完了。你可能以前曾經(jīng)這樣寫過 React 代碼。但這并不該是你最終所該做的(不過這樣做也還湊合),但是它很不 “DRY”。正如我們所看到的,我們在我們的每一個組件中都 重復著完全一樣的的鼠標懸停邏輯。
從這點看的話,問題變得非常清晰了:我們希望避免在在每個需要添加鼠標懸停邏輯的組件是都再寫一遍相同的邏輯。所以,解決辦法是什么?在我們開始前,讓我們先討論一些能讓我們更容易理解答案的編程思想—— 回調函數(shù) 和 高階函數(shù)。
在 JavaScript 中,函數(shù)是 “一等公民”。這意味著它就像對象/數(shù)組/字符串那樣可以被聲明為一個變量、當作函數(shù)的參數(shù)或者在函數(shù)中返回一個函數(shù),即使返回的是其他函數(shù)也可以。
function add (x, y) { return x + y}function addFive (x, addReference) { return addReference(x, 5)}addFive(10, add) // 15復制代碼如果你沒這樣用過,你可能會感到困惑。我們將 add 函數(shù)作為一個參數(shù)傳入 addFive 函數(shù),重新命名為 addReference,然后我們調用了著個函數(shù)。
這時候,你作為參數(shù)所傳遞進去的函數(shù)被叫做回調函數(shù)同時你使用回調函數(shù)所構建的新函數(shù)被叫做高階函數(shù)。
因為這些名詞很重要,下面是一份根據(jù)它們所表示的含義重新命名變量后的同樣邏輯的代碼。
function add (x,y) { return x + y}function higherOrderFunction (x, callback) { return callback(x, 5)}higherOrderFunction(10, add)復制代碼這個模式很常見,哪里都有它。如果你之前用過任何 JavaScript 數(shù)組方法、jQuery 或者是 lodash 這類的庫,你就已經(jīng)用過高階函數(shù)和回調函數(shù)了。
[1,2,3].map((i) => i + 5)_.filter([1,2,3,4], (n) => n % 2 === 0 );$('#btn').on('click', () => console.log('回調函數(shù)哪里都有'))復制代碼讓我們回到我們之前的例子。如果我們不僅僅想創(chuàng)建一個 addFive 函數(shù),我們也想創(chuàng)建 addTen函數(shù)、 addTwenty 函數(shù)等等,我們該怎么辦?在我們當前的實踐方法中,我們必須在需要的時候去重復地寫我們的邏輯。
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) // 15addTen(10, add) // 20addTwenty(10, add) // 30復制代碼再一次出現(xiàn)這種情況,這樣寫并不糟糕,但是我們重復寫了好多相似的邏輯。這里我們的目標是要能根據(jù)需要寫很多 “adder” 函數(shù)(addFive、addTen、addTwenty 等等),同時盡可能減少代碼重復。為了完成這個目標,我們創(chuàng)建一個 makeAdder 函數(shù)怎么樣?著個函數(shù)可以傳入一個數(shù)字和原始 add 函數(shù)。因為這個函數(shù)的目的是創(chuàng)建一個新的 adder 函數(shù),我們可以讓其返回一個全新的傳遞數(shù)字來實現(xiàn)加法的函數(shù)。這兒講的有點多,讓我們來看下代碼吧。
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) // 15addTen(10) // 20addTwenty(10) // 30復制代碼太酷了!現(xiàn)在我們可以在需要的時候隨意地用最低的代碼重復度創(chuàng)建 “adder” 函數(shù)。
如果你在意的話,這個通過一個多參數(shù)的函數(shù)來返回一個具有較少參數(shù)的函數(shù)的模式被叫做 “部分應用(Partial Application)“,它也是函數(shù)式編程的技術。JavaScript 內(nèi)置的 “.bind“ 方法也是一個類似的例子。
好吧,那這與 React 以及我們之前遇到鼠標懸停的組件有什么關系呢?我們剛剛通過創(chuàng)建了我們的 makeAdder 這個高階函數(shù)來實現(xiàn)了代碼復用,那我們也可以創(chuàng)建一個類似的 “高階組件” 來幫助我們實現(xiàn)相同的功能(代碼復用)。不過,不像高階函數(shù)返回一個新的函數(shù)那樣,高階組件返回一個新的組件來渲染 “回調” 組件
作者:
黑馬程序員前端與移動開發(fā)培訓學院首發(fā):
http://web.itheima.com/?v2