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

KayleneNCMT 7年前
   <p>使用 React 开发应用,给予了前端工程师无限“组合拼装”快感。但在此基础上,组件如何划分,数据如何流转等应用设计都决定了代码层面的美感和强健性。</p>    <p>同时,在 React 世界里提到 curry 化,也许很多开发者会第一时间反应出 React-redux 库的 connect 方法。然而,如果仅仅机械化地停留于此,而没有更多灵活地应用,是非常可惜的。</p>    <p>这篇文章以一个真实场景为基础,从细节出发,分析 curry 化如何化简为繁,更优雅地实现需求。</p>    <h2>场景介绍</h2>    <p>需求场景为一个卖食品的电商网站,左侧部分为商品筛选栏目,用户可以根据:价格区间、商品年限、商品品牌进行过滤。右侧展现对应产品。如下图:</p>    <p><img src="https://simg.open-open.com/show/54a1739e93f7edd581b3cd8df246776d.png"></p>    <p>作为 React 开发者,我们知道 React 是组件化的,第一步将考虑根据 UE 图,进行组件拆分。这个过程比较简单直观,我们对拆分结果用下图表示:</p>    <p><img src="https://simg.open-open.com/show/5509884fed2e824e832ed48a66e3af2b.png"></p>    <p>对应代码为:</p>    <pre>  <code class="language-javascript"><Products>      <Filters>          <PriceFilter/>          <AgeFilter/>          <BrandFilter/>      </Filters>      <ProductResults/>  </Products></code></pre>    <h2>初级实现</h2>    <p>React 是基于数据状态的,紧接着第二步就要考虑应用状态。商品展现结果数据我们暂时不需要关心。这里主要考虑应用最重要的状态,即 <strong>过滤条件信息</strong> 。</p>    <p>我们使用命名为 filterSelections 的 JavaScript 对象表示过滤条件信息,如下:</p>    <pre>  <code class="language-javascript">filterSelections = {    price: ...,    ages: ...,    brands: ...,  }</code></pre>    <p>此数据需要在 Products 组件中进行维护。因为 Products 组件的子组件 Filters 和 ProductResults 都将依赖这项数据状态。</p>    <p>Filters 组件通过 prop 接收 filterSelections 状态,并拆解传递给它的三项筛选子组件:</p>    <pre>  <code class="language-javascript">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>      );    };  }</code></pre>    <p>同样地,ProductResults 组件也通过 prop 接收 filterSelections 状态,进行相应产品的展示。</p>    <p>对于 Filters 组件,它一定不仅仅是接收 filterSelections 数据而已,同样也需要对此项数据进行更新。为此,我们在 Products 组件中设计相应的 handler 函数,对过滤信息进行更新,命名为 updateFilters,并将此处理函数作为 prop 下发给 Filters 组件:</p>    <pre>  <code class="language-javascript">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>      );    }  }</code></pre>    <p>注意这里我们对 this 绑定方式。有兴趣的读者可以参考我的另一篇文章: <a href="/misc/goto?guid=4959757663095349736" rel="nofollow,noindex">从 React 绑定 this,看 JS 语言发展和框架设计</a> 。</p>    <p>作为 Filters 组件,同样也要对处理函数进行进一步拆分和分发:</p>    <pre>  <code class="language-javascript">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>      );    };  }</code></pre>    <p>我们根据 selectionsChanged 函数,通过传递不同类型参数,设计出 updatePriceFilter、updateAgeFilter、updateBrandFilter 三个方法,分别传递给 PriceFilter、AgeFilter、BrandFilter 三个组件。</p>    <p>这样的做法非常直接,然而运行良好。但是在 Filters 组件中,多了很多函数,且这些函数看上去做着相同的逻辑。如果将来又多出了一个或多个过滤条件,那么同样也要多出同等数量的“双胞胎”函数。这显然不够优雅。</p>    <h2>currying 是什么</h2>    <p>在分析更加优雅的解决方案之前,我们先简要了解一下 curry 化是什么。curry 化事实上是一种变形,它将一个函数 f 变形为 f',f' 的参数接收原本函数 f 的参数,同时返回一个新的函数 f'',f'' 接收剩余的参数并返回函数 f 的计算结果。</p>    <p>这么描述无疑是抽象的,我们还是通过代码来理解。这是一个简单的求和函数:</p>    <pre>  <code class="language-javascript">add = (x, y) => x + y;</code></pre>    <p>curried 之后:</p>    <pre>  <code class="language-javascript">curriedAdd = (x) => {    return (y) => {      return x + y;    }  }</code></pre>    <p>所以,当执行 curriedAdd(1)(2) 之后,得到结果 3,curriedAdd(x) 函数有一个名字叫 partial application,curriedAdd 函数只需要原本 add(X, y) 函数的一部分参数。</p>    <p>Currying a regular function let’s us perform partial application on it.</p>    <h2>curry 化应用</h2>    <p>再回到之前的场景,我们设计 curry 化函数:updateSelections,</p>    <pre>  <code class="language-javascript">updateSelections = (selectionType) => {    return (newValue) => {      this.props.selectionsChanged({        ...this.props.filterSelections,        [selectionType]: newValue,      });    }  };</code></pre>    <p>进一步可以简化为:</p>    <pre>  <code class="language-javascript">updateSelections = (selectionType) => (newValue) => {     this.props.selectionsChanged({        ...this.props.filterSelections,        [selectionType]: newValue,     })  };</code></pre>    <p>对于 updateSelections 的偏应用(即上面提到的 partial application):</p>    <pre>  <code class="language-javascript">updateSelections('ages');  updateSelections('brands');  updateSelections('price');</code></pre>    <p>相信大家已经理解了这么做的好处。这样一来,我们的 Filters 组件完整为:</p>    <pre>  <code class="language-javascript">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>      );    };  }</code></pre>    <p>当然,currying 并不是解决上述问题的唯一方案。我们再来了解一种方法,进行对比消化,updateSelections 函数 uncurried 版本:</p>    <pre>  <code class="language-javascript">updateSelections = (selectionType, newValue) => {    this.props.updateFilters({      ...this.props.filterSelections,      [selectionType]: newValue,    });  }</code></pre>    <p>这样的设计使得每一个 Filter 组件:PriceFilter、AgeFilter、BrandFilter 都要调用 updateSelections 函数本身,并且要求组件本身感知 filterSelections 的属性名,以进行相应属性的更新。这就是一种耦合,完整实现:</p>    <pre>  <code class="language-javascript">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)}               />            </>          );        };      }</code></pre>    <p>其实我认为,在这种场景下,关于两种方案的选择,可以根据开发者的偏好来决定。</p>    <h2>总结</h2>    <p>这篇文章内容较为基础,但从细节入手,展现了 React 开发编写和函数式理念相结合的魅力。文章 <a href="/misc/goto?guid=4959757663174592448" rel="nofollow,noindex">译自这里</a> ,部分内容有所改动。</p>    <p>广告时间:</p>    <p>如果你对前端发展,尤其对 React 技术栈感兴趣:我的新书中,也许有你想看到的内容。关注作者 <a href="/misc/goto?guid=4959757663263068750" rel="nofollow,noindex">Lucas HC</a> ,新书出版将会有送书活动。</p>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000014458607</p>    <p> </p>