使用 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 组件中,多了不少函数,且这些函数看上去作着相同的逻辑。若是未来又多出了一个或多个过滤条件,那么一样也要多出同等数量的“双胞胎”函数。这显然不够优雅。
在分析更加优雅的解决方案以前,咱们先简要了解一下 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 化函数: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团队设计思想