少妇白洁系列之React StateUp模式

“换句话说,StateUp模式把面向对象的设计方法应用到了状态对象的管理上,在遵循React的组件化机制和基于props实现组件通信方式的前提之下作到了这一点。” ---- 少妇白洁javascript

阅读本文以前,请肯定你读过React的官方文档中关于Lifting State Up的论述:html

https://facebook.github.io/re...java

昨天写的雏形通过refine以后,获得了React有史以来最激动人心的代码模式之一。react

咱们的出发点很简单:git

  1. 但愿把有态组件的态提高到父组件管理,这个过程应该能够向上递归,即状态能够层层提高;github

  2. 在代码形式层面,应该和原生React组件越兼容越好;segmentfault

现有的React有态组件改为状态提高组件(StateUp)应该很简单,反之亦然;StateUp组件有时可能会须要象普通React组件那样使用;一个最初没有子组件的StateUp组件,可能会加入StateUp子组件;在这些设计变动发生时,组件的修改和组合都应该很简单,且灵活。数组

子组件

咱们首先考虑子组件,子组件的约定是:函数

1 继承自React.PureComponent组件化

这个新出现不久的便捷组件在scu时自动作shallow equal的比较,省去本身写代码的麻烦;

2 只需修改this.statethis.setStatethis.props.statethis.props.setState

这样切换代码模式时很是简单;this.props.setState的语义实现和React组件原有的this.setState一致,即merge状态对象而不是replace;

3 原来写在this.state内的对象,成为独立的JavaScript类对象;设计为类对象而不是Plain Object的好处是它能够有方法,便于重用;这个类确定和组件类成对使用,因此不妨把它直接嵌入成为组件类的static成员,统一命名为State

即每一个StateUp组件看起来这样:

class MyStateUp extends PureComponent {
  static State = class State {
    constructor() {
      this.something = ...
    }
  }
}

写在class关键字以后的State不是必须的,可是给这个类赋一个类名的好处是在实现类方法时能够直接调用类构造函数建立新对象,不然这个构造函数没有名字。

父组件

父组件须要经过props向子组件传递两个东西,第一个是子组件的state对象,第二个是子组件须要的setState方法;目的是能够大概写成:

<SubComponent state={...} setState={...} />

前者比较容易实现,后者有一点小麻烦;

通常咱们向子组件传递方法时都是用bound function,绑定父组件的this;若是直接写在render方法的JSX语法内,每次建立的bound function对象实例是不一样的,这致使每次父组件render,子组件都要从新render,这不是咱们想要的结果;因此咱们须要一个在父组件上持久化的对象成员提供这个bound function。通常这种状况会在类的构造函数内建立一个属性,引用bound function或词法域bind this的arrow function,但后面会看到咱们有更好的办法,避免这种手工代码。

确切的说,这里说的父组件或者子组件指的是相对角色(role);角色和组件是否为StateUp组件无关;一个StateUp组件能够是其余StateUp组件的父组件,同时也是另外一个StateUp组件的子组件;即StateUp组件是能够组合的。

做为“子”的责任是前面说的提供内嵌static Class用于构造原有this.state对象,以及继承自PureComponent

做为“父”的责任是组合子对象的状态,同时具备一些类方法,能够向子组件提供类对象和类对象更新方法;

若是一个组件兼具二者,它能够继续向上组合;若是对象只具备后者特性,它是一个普通的React组件,但能够内部使用StateUp组件。

StateUp mixin

StateUp mixin能够赋予一个StateUp组件,或者普通React有态组件,成为“父”组件所需的类方法。

StateUp的代码很是简单,实际上它是一个纯函数,向一个React组件内混入(mixin)三个方法:

  1. setSubState 更新某个子组件的状态对象;父组件能够直接调用这个方法;

  2. setSubStateBound 提供一个稳定的bound function,传递给子组件,子组件能够用来更新托管的状态对象;

  3. stateBinding 返回一个props对象,结合spread operator使书写JSX更方便;

StateUp mixin自己并不依赖React。

const StateUp = base => class extends base {

  setSubState(name, nextSubState) {
    let state = this.props.state || this.state
    let subState = state[name]
    let nextSubStateMerged = Object.assign(new subState.constructor(), subState, nextSubState)
    let nextState = { [name]: nextSubStateMerged }
    this.props.setState
      ? this.props.setState(nextState)
      : this.setState(nextState)
  }

  setSubStateBound(name) {
    let obj = this.setSubStateBoundObj || (this.setSubStateBoundObj = {})
    return obj[name] ? obj[name] : (obj[name] = this.setSubState.bind(this, name))
  }

  stateBinding(name) {
    return {
      state: this.props.state ? this.props.state[name] : this.state[name],
      setState: this.setSubStateBound(name)
    }
  }
}

父组件和子组件的状态约定:父组件使用一个object来做为状态容器,其中一个property对应一个子组件,该property引用的对象就是子组件的状态对象;全部状态对象都是class对象;这些对象构成的树,就是StateUp组件经过组合构成的树,不一样之处在于对象的生命周期是超过对应组件的生命周期的;在JSX语法中常常根据须要显示或删除某个组件,但该组件对应的状态对象,能够在父组件的状态对象内持久;

setSubState方法是用于更新子组件状态的方法;

setSubState的第一个参数是(property) name,第二个参数是子组件提供的nextState,按照React习惯,它是partial state,应merge到子组件的状态中;

setSubState代码第一句拿到父组件的state,若是父组件是有态的就是this.state,若是父组件的态托管到更高的组件上去,就是this.props.state

第二句拿到子组件的state;

第三句先构造一个新的子组件状态对象,注意它是new出来的,不是{};而后子组件的当前状态和新的状态都merge进去,获得父组件的nextState对象(也是partial state);

最后一句更新状态,若是父组件也是StateUp组件,它继续调用父父组件方法更新状态,若是不是,它直接调用React组件的setState方法,二者的语义被设计成一致的;

本质上这个函数是一个递归函数;React的this.setState最终必定会被调用,但在此以前,一直是this.props.setState在沿着StateUp组件的层级关系自下向上调用;

若是熟悉immutable的话,会发现StateUp组件的状态对象树是知足immutable要求的,由于在计算nextSubStateMerged时使用了Object.assign();因此这个递归过程沿着对象路径一直到root都会更新,因为全部StateUp组件都是继承自PureComponent,自带的SCU,其结果是:

  1. 按照React的设计逻辑,这是最佳性能;

  2. PureComponent自带SCU逻辑,不须要手工代码;

  3. 没有额外的state store, dispatch, action/event之类的逻辑,只使用React组件和JavaScript类对象来维护状态;

  4. 真正会触发render的setState只在顶级父组件中被调用一次;不是组件和状态管理器之间binding以后的触发多个组件的render。

setSubStateBound用于产生和获取父组件向子组件传递的setState bound function

它首先在父组件上建立名字为setSubStateBoundObj的容器,用于装载bound function;通常使用而言不必用Map,就用Plain Object作key value容器便可;

该函数采用了Lazy方式工做,即每次须要时才建立bound function;这个作法优于试图在构造函数中建立这些bound function的作法,后者或者须要用户手工代码,或者须要使用Decorator去Hack class的构造函数,没有必要的复杂、危险、或不兼容;

该函数为每一个子组件建立一个bound function(即子组件的this.props.setState);它除了bind this以外还须要bind (property) name;建立的bound function保存在容器内,每次调用setSubStateBound时返回结果一致,确保了全部子组件的SCU工做。

最后的stateBinding方法是便利函数,用于书写JSX时不须要手写statesetState,使用spread operator便可;

<SubComponent {...this.stateBinding('subComponentName')} />

解耦

StateUp的核心是实现了组件(view)和它的状态对象(state)的解耦;

StateUp组件的状态对象是class对象,这个class是JavaScript class,与React无关,它能够提供各类方法;

其中的只读方法,对父组件而言能够直接访问,这对于父组件协调几种子组件的显示很是有用,例如按钮的使能;

class对象也能够有改变对象状态的方法,但约定是,它只能返回新对象,即保证immutable;对于子组件而言,这些方法能够直接调用,而后经过this.props.setState实现更新;对于父组件而言这些方法一样能够调用,但更新路径是this.setSubState

后者至关于在普通的React组件组合中,父组件在拿到子组件的引用后直接去调用子组件的setState方法;

在React中这不是值得推荐的方法(洗剪吹们称之为anti-pattern),首先是由于React的setState没有封装可言,调用该方法须要理解组件内部的状态的含义,其次子组件很动态,父组件容易拿到旧的子组件的引用致使错误;

StateUp组件的状态对象从组件中剥离出去,它解耦了“须要更新状态以更新显示”和“了解如何更新状态”这两件事情,后者用类对象方法实现封装,而父组件能够只完成前者;

例如一个输入框对应的状态对象可能有一个叫作reset的方法,父组件能够调用reset方法得到一个新的子组件状态对象,父组件仅更新对象便可,它不须要了解reset如何工做;reset方法写在状态对象类上,自己也能够最大限度的重用;

另一个例子,考虑一个Form和向导组件;Form中的一些元素,例如用户的用户名和密码输入框,若是输入合法,则next button使能;

StateUp模式中,用户名密码输入能够做为一个组件封装,它的状态对象能够提供ready方法用于判断是否完成,这比在组件上提供props和传递bound function通知方便,父组件也不须要cache一个ready状态;

一样的,若是next以后,用户名密码组件从视图中移除,父组件须要保存以前输入的用户名密码副本,若是用户回退,这些内容还要交给新建立的用户名密码组件,而在StateUp模式下,这些都不是问题,由于子组件状态对象并未消除,它也不须要把内容展开到父组件的状态容器内,若是父组件须要获取输入结果,那么一个get方法便可作到;

这样的组件不管在向导页面、用户修改用户名密码的页面等等地方都很容易重用,父组件仅仅须要在本身的状态内建立一个子组件的状态对象便可,在render时也仅仅须要传递这个对象而不是展开的一组props,也不须要去增长不少接受状态变化通知的方法并传递到子组件上;

换句话说,StateUp模式把面向对象的设计方法应用到了状态对象的管理上,在遵循React的组件化机制和基于props实现组件通信方式的前提(context)之下作到了这一点。

可以在组件的状态对象上实现维护状态的方法,父子组件都可访问,均有更新路径,是StateUp模式的重要的收益,它兼顾了便利性、灵活性、和代码重用。

完整例子

下面看一个简单且无聊的代码实例:

class Sub extends PureComponent {

  static State = class State {
    constructor() {
      this.label = ''
    }
  }

  render() {

    console.log('Sub render:' + this.props.state.label)

    return (
      <div>
        <button
          style={{width: 64, height: 24}}
          onClick={() => this.props.setState({ label: this.props.state.label + 'a' })}
        >
          {this.props.state.label}
        </button>
      </div>
    )
  }
}

能够看到对子组件而言没有由于Pattern引入带来的过多代码负担;StateUp组件须要提供状态对象的class,必须写成static且名字为State;这是一个约定;父组件利用这个约定找到子组件的构造函数建立子组件的状态对象;

下面的代码展现了如何在父组件中使用子组件;这个父组件自己也是一个StateUp组件,即继续向上层容器传递状态,而不是本身维护状态;

class Composite extends StateUp(PureComponent) {

  static State = class State {
    constructor() {
      this.sub1 = new Sub.State()
      this.sub2 = new Sub.State()
      this.sub3 = new Sub.State()
    }
  }

  render() {

    return (
      <div>
        <Sub {...this.stateBinding('sub1') } />
        <Sub {...this.stateBinding('sub2') } />
        <Sub {...this.stateBinding('sub3') } />
      </div>
    )
  }
}

注意extends关键字后面的写法,这是目前为止JavaScript里最好的mixin模式,它不污染prototype,也没有由于前卫的语法致使兼容性问题,StateUp自己不重载constructor,也不会影响super,instanceof等关键字的使用;

Composite也是StateUp组件,也要提供一个State class,其构造函数中调用Sub.State类的构造函数构造子组件的状态对象,用sub1, sub2sub3命名;

render方法里展现了子组件的使用方式;这里应该看做是一种binding;把一个组件的状态对象和它的view binding在一块儿,后者是pure的。

Composite仍然是StateUp组件,这意味着若是要使用它须要一个更上层的容器;咱们来写一个通用的组件终结这个层层向上传递状态的游戏。

class Stateful extends StateUp(Component) {

  constructor(props) {
    super()
    this.state = { state: new props.component.State() }
  }

  render() {
    let Component = this.props.component
    return <Component {...this.stateBinding('state')} />
  }
}

class App extends Component {
  render() {
    return <Stateful component={Composite} />
  }
}

Stateful仍然须要继承StateUp mixin,这样它就有在内部组合使用StateUp组件的便利;可是它不用PureComponent作起点,而使用标准的React Component,它是有态的,也是它下面全部StateUp组件树的惟一顶层态容器。

Stateful不须要static State class,它直接用本身的this.state做为状态容器;因为StateUp代码里要求子组件状态对象在父组件状态对象中必须有名字,因此这里在this.state内再建立一个叫state的property,引用子组件状态对象(这样能够重用代码);

Stateful是通用的,它具体wrap了哪一个StateUp组件,用名字为component的prop传递进来,在render方法里直接渲染这个component便可;

最终咱们在示例代码中用Stateful把Composite用在页面上。

上述代码很容易调试;在Sub组件的render方法中有一句打印,能够看到在每次点击button时只有该button会渲染,即全部StateUp,做为PureComponent,SCU自动工做;

小结

目前的代码只能用对象方式组合,不能用数组,但这不是一个很大的麻烦,若是你仔细看StateUp mixin函数代码就会发现,name改为index是很容易的,只是bound function的处理方式要当心,由于它在对象被销毁以前没有回收机制。

这个Pattern不是为了做为大一统的状态管理器被提出的;我最初只想实现一些反复重写的代码的重用;

React自己经过Composition的重用,在理论上没有问题,但很是不灵活;虽然有container component和pure component的概念,可是container component的状态变化,仍然须要在更高层的组件内cache状态,cache的更新经过props传递notification实现,这造成了一个两难局面:若是状态local,则组件的props设计须要考虑可能的观察者逻辑,若是状态提高,则破坏封装原则;

StateUp模式就是为了解决这个问题设计的;它给出了一种方式让子组件既能独立封装逻辑,便于重用,又能绕开写起来很是繁琐的props通信机制,让父组件方便获取子组件状态,灵活组合行为;

StateUp组件的重用能力是卓越的,你不须要把状态和维护状态的逻辑代码放到另一个文件里;StateUp也没有外部依赖,不强制要求消息总线或状态管理器,没有所以致使的性能问题,binding问题,消息名称的namespace问题;它是百分之百纯JS和百分之百纯React;

它在性能上,以及为了获取这种性能所须要的额外编码上,也接近完美。

事实上,我我的认为,既然React都有了PureComponent做为内置组件,这种StateUp模式,也应该是React内置功能。

更新

  1. 最新关于React StateUp模式的数学背景介绍: https://segmentfault.com/a/11...

  2. subProps函数重命名为stateBinding,由于本质上它是向子组件绑定状态和更新状态的方法;

相关文章
相关标签/搜索