微信号:gh_363dd91d24f5

介绍:专注于Web前端;分享 HTML/CSS/JS 等原创知识

React 应用设计之道 - curry 化妙用

2018-06-13 18:23 Lucas HC

使用 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!

 
颜海镜 更多文章 组件复用那些事儿 - React 实现按需加载轮子 多环境管理终极方案 探索 Redux4.0 版本迭代 论基础谈展望(对比 React contex React 模态框秘密和“轮子”渐进设计 React 探秘 - React Component 和 Eleme
猜您喜欢 频繁改需求,程序猿的应对绝招! 如何使用postgis做一个高可用的附近的人服务? 如何测得整条IO路径上的并发度? 天猫即将不支持IE8 (十五)Android 文本控件style的修饰-BabushkaText