React 應用設計之道 - curry 化妙用

使用 React 開發應用,給予了前端工程師無限「組合拼裝」快感。但在此基礎上,組件如何劃分,數據如何流轉等應用設計都決定了代碼層面的美感和強健性。前端

同時,在 React 世界裏提到 curry 化,也許不少開發者會第一時間反應出 React-redux 庫的 connect 方法。然而,若是僅僅機械化地停留於此,而沒有更多靈活地應用,是很是惋惜的。react

這篇文章以一個真實場景爲基礎,從細節出發,分析 curry 化如何化簡爲繁,更優雅地實現需求。git

場景介紹

需求場景爲一個賣食品的電商網站,左側部分爲商品篩選欄目,用戶能夠根據:價格區間、商品年限、商品品牌進行過濾。右側展示對應產品。以下圖:github

頁面示意圖

做爲 React 開發者,咱們知道 React 是組件化的,第一步將考慮根據 UE 圖,進行組件拆分。這個過程比較簡單直觀,咱們對拆分結果用下圖表示:redux

組件設計

對應代碼爲:promise

<Products>
	<Filters>
		<PriceFilter/>
		<AgeFilter/>
		<BrandFilter/>
	</Filters>
	<ProductResults/>
</Products>
複製代碼

初級實現

React 是基於數據狀態的,緊接着第二步就要考慮應用狀態。商品展示結果數據咱們暫時不須要關心。這裏主要考慮應用最重要的狀態,即過濾條件信息前端工程師

咱們使用命名爲 filterSelections 的 JavaScript 對象表示過濾條件信息,以下:app

filterSelections = {
  price: ...,
  ages: ...,
  brands: ...,
}
複製代碼

此數據須要在 Products 組件中進行維護。由於 Products 組件的子組件 Filters 和 ProductResults 都將依賴這項數據狀態。框架

Filters 組件經過 prop 接收 filterSelections 狀態,並拆解傳遞給它的三項篩選子組件:函數

class Filters extends React.Component {
  render() {
    return (
      <div>
        <PriceFilter price={this.props.filterSelections.price} />
        <AgeFilter ages={this.props.filterSelections.ages} />
        <BrandFilter brands={this.props.filterSelections.brands} />
      </div>
    );
  };
}
複製代碼

一樣地,ProductResults 組件也經過 prop 接收 filterSelections 狀態,進行相應產品的展現。

對於 Filters 組件,它必定不只僅是接收 filterSelections 數據而已,一樣也須要對此項數據進行更新。爲此,咱們在 Products 組件中設計相應的 handler 函數,對過濾信息進行更新,命名爲 updateFilters,並將此處理函數做爲 prop 下發給 Filters 組件:

class Products extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterSelections: {
        price: someInitialValue,
        ages: someInitialValue,
        brands: someInitialValue,
      }
    }
  }

  updateFilters = (newSelections) => {
    this.setState({
      filterSelections: newSelections
    })
  };

  render() {
    return(
      <div>
        <Filters 
          filterSelections={this.state.filterSelections}
          selectionsChanged={this.updateFilters}
        />
        <Products filterSelections={this.state.filterSelections} />
      </div>
    );
  }
}
複製代碼

注意這裏咱們對 this 綁定方式。有興趣的讀者能夠參考個人另外一篇文章:從 React 綁定 this,看 JS 語言發展和框架設計

做爲 Filters 組件,一樣也要對處理函數進行進一步拆分和分發:

class Filters extends React.Component {
  updatePriceFilter = (newValue) => {
    this.props.selectionsChanged({
      ...this.props.filterSelections,
      price: newValue
    })
  };

  updateAgeFilter = (newValue) => {
    this.props.selectionsChanged({
      ...this.props.filterSelections,
      ages: newValue
    })
  };

  updateBrandFilter = (newValue) => {
    this.props.selectionsChanged({
      ...this.props.filterSelections,
      brands: newValue
    })
  };
  
  render() {
    return (
      <div>
        <PriceFilter 
          price={this.props.filterSelections.price} 
          priceChanged={this.updatePriceFilter} 
        />
        <AgeFilter 
          ages={this.props.filterSelections.ages} 
          agesChanged={this.updateAgeFilter} 
        />
        <BrandFilter 
          brands={this.props.filterSelections.brands} 
          brandsChanged={this.updateBrandFilter} 
        />
      </div>
    );
  };
}
複製代碼

咱們根據 selectionsChanged 函數,經過傳遞不一樣類型參數,設計出 updatePriceFilter、updateAgeFilter、updateBrandFilter 三個方法,分別傳遞給 PriceFilter、AgeFilter、BrandFilter 三個組件。

這樣的作法很是直接,然而運行良好。可是在 Filters 組件中,多了不少函數,且這些函數看上去作着相同的邏輯。若是未來又多出了一個或多個過濾條件,那麼一樣也要多出同等數量的「雙胞胎」函數。這顯然不夠優雅。

currying 是什麼

在分析更加優雅的解決方案以前,咱們先簡要了解一下 curry 化是什麼。curry 化事實上是一種變形,它將一個函數 f 變形爲 f',f' 的參數接收本來函數 f 的參數,同時返回一個新的函數 f'',f'' 接收剩餘的參數並返回函數 f 的計算結果。

這麼描述無疑是抽象的,咱們仍是經過代碼來理解。這是一個簡單的求和函數:

add = (x, y) => x + y;
複製代碼

curried 以後:

curriedAdd = (x) => {
  return (y) => {
    return x + y;
  }
}
複製代碼

因此,當執行 curriedAdd(1)(2) 以後,獲得結果 3,curriedAdd(x) 函數有一個名字叫 partial application,curriedAdd 函數只須要本來 add(X, y) 函數的一部分參數。

Currying a regular function let’s us perform partial application on it.

curry 化應用

再回到以前的場景,咱們設計 curry 化函數:updateSelections,

updateSelections = (selectionType) => {
  return (newValue) => {
    this.props.selectionsChanged({
      ...this.props.filterSelections,
      [selectionType]: newValue,
    });
  }
};
複製代碼

進一步能夠簡化爲:

updateSelections = (selectionType) => (newValue) => {
   this.props.selectionsChanged({
      ...this.props.filterSelections,
      [selectionType]: newValue,
   })
};
複製代碼

對於 updateSelections 的偏應用(即上面提到的 partial application):

updateSelections('ages');
updateSelections('brands');
updateSelections('price');
複製代碼

相信你們已經理解了這麼作的好處。這樣一來,咱們的 Filters 組件完整爲:

class Filters extends React.Component {
  
  updateSelections = (selectionType) => {
    return (newValue) => {
      this.props.selectionsChanged({
        ...this.props.selections,
        [selectionType]: newValue,  // new ES6 Syntax!! :)
      });
    }
  };

  render() {
    return (
      <div>
        <PriceFilter 
          price={this.props.selections.price} 
          priceChanged={this.updateSelections('price')} 
        />
        <AgeFilter 
          ages={this.props.selections.ages} 
          agesChanged={this.updateSelections('ages')} 
        />
        <BrandFilter 
          brands={this.props.selections.brands} 
          brandsChanged={this.updateSelections('brands')} 
        />
      </div>
    );
  };
}
複製代碼

固然,currying 並非解決上述問題的惟一方案。咱們再來了解一種方法,進行對比消化,updateSelections 函數 uncurried 版本:

updateSelections = (selectionType, newValue) => {
  this.props.updateFilters({
    ...this.props.filterSelections,
    [selectionType]: newValue,
  });
}
複製代碼

這樣的設計使得每個 Filter 組件:PriceFilter、AgeFilter、BrandFilter 都要調用 updateSelections 函數自己,而且要求組件自己感知 filterSelections 的屬性名,以進行相應屬性的更新。這就是一種耦合,完整實現:

class Filters extends React.Component {

	  updateSelections = (selectionType, newValue) => {
	    this.props.selectionsChanged({
	      ...this.props.filterSelections,
	      [selectionType]: newValue, 
	    });
	  };
	
	  render() {
	    return (
	      <>
	        <PriceFilter 
	          price={this.props.selections.price} 
	          priceChanged={(value) => this.updateSelections('price', value)} 
	        />
	        <AgeFilter 
	          ages={this.props.selections.ages} 
	          agesChanged={(value) => this.updateSelections('ages', value)} 
	        />
	        <BrandFilter 
	          brands={this.props.selections.brands} 
	          brandsChanged={(value) => this.updateSelections('brands', value)} 
	        />
	      </>
	    );
	  };
	}
複製代碼

其實我認爲,在這種場景下,關於兩種方案的選擇,能夠根據開發者的偏好來決定。

總結

這篇文章內容較爲基礎,但從細節入手,展示了 React 開發編寫和函數式理念相結合的魅力。文章譯自這裏,部份內容有所改動。

廣告時間: 若是你對前端發展,尤爲對 React 技術棧感興趣:個人新書中,也許有你想看到的內容。關注做者 Lucas HC,新書出版將會有送書活動。

Happy Coding!

PS: 做者 Github倉庫 和 知乎問答連接 歡迎各類形式交流!

個人其餘幾篇關於React技術棧的文章:

從setState promise化的探討 體會React團隊設計思想

從setState promise化的探討 體會React團隊設計思想

經過實例,學習編寫 React 組件的「最佳實踐」

React 組件設計和分解思考

從 React 綁定 this,看 JS 語言發展和框架設計

作出Uber移動網頁版還不夠 極致性能打造才見真章**

React+Redux打造「NEWS EARLY」單頁應用 一個項目理解最前沿技術棧真諦**

相關文章
相關標籤/搜索