使用 React 開發應用,給予了前端工程師無限“組合拼裝”快感。但在此基礎上,元件如何劃分,資料如何流轉等應用設計都決定了程式碼層面的美感和強健性。
同時,在 React 世界裡提到 curry 化,也許很多開發者會第一時間反應出 React-redux 庫的 connect 方法。然而,如果僅僅機械化地停留於此,而沒有更多靈活地應用,是非常可惜的。
這篇文章以一個真實場景為基礎,從細節出發,分析 curry 化如何化簡為繁,更優雅地實現需求。
場景介紹
需求場景為一個賣食品的電商網站,左側部分為商品篩選欄目,使用者可以根據:價格區間、商品年限、商品品牌進行過濾。右側展現對應產品。如下圖:
作為 React 開發者,我們知道 React 是元件化的,第一步將考慮根據 UE 圖,進行元件拆分。這個過程比較簡單直觀,我們對拆分結果用下圖表示:
對應程式碼為:
<Products>
<Filters>
<PriceFilter/>
<AgeFilter/>
<BrandFilter/>
</Filters>
<ProductResults/>
</Products>
複製程式碼
初級實現
React 是基於資料狀態的,緊接著第二步就要考慮應用狀態。商品展現結果資料我們暫時不需要關心。這裡主要考慮應用最重要的狀態,即過濾條件資訊。
我們使用命名為 filterSelections 的 JavaScript 物件表示過濾條件資訊,如下:
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團隊設計思想