React Component里的状态机Pattern

State Machine in React Component

React的工程实践中大多数团队都只关注了state该怎么存放的问题,没有意识到真正致使问题复杂的是组合状态机,后面这句话对于UI而言是放之四海皆准的;javascript

一个React Component对象做为UI层元素,在不少状况下咱们并不但愿在状态迁移时建立新的实例替代旧的,这直接意味着UI组件和状态机之间是binding关系而不是composition,因此React提供了一个this.state用于解耦,这是它很聪明的一个设计;可是这个this.state只有值成员,没有方法成员;这意味着写在Component上的方法里面要switch/case状态,这很是不方便。前端

其次React Component的setState方法是merge逻辑而不是replace逻辑,它意味着state下一级props之间必须是平行子状态机而不是单一状态机互斥状态(除非你只有一个状态机,其余状态用值表示);或者换句话说,若是你把不一样的互斥状态下的资源和值都放在一个篮子里时,你每次本身去手动倒空旧的,这一点是个坑。java

第三,那些early binding语言的状态机Pattern在js和immutable要求下并不适用,他们都是内部值状态的迁移而不是对象自己被替代,而对象自己被替代这个问题制造了一个问题,就是该对象的方法并不能用于UI的行为binding,由于状态迁移后这个旧状态机对象就废弃了,调用它的行为方法固然是不对的;node

解决这个问题并不难,行为binding使用Component对象上的方法,它是稳定的,不会由于model的状态机更迭而变化,但它是一个proxy,须要把方法分发到子状态机上;这样咱们就获得了状态机Pattern的最大优点:每一个状态只关注属于本身的子状态,值,资源,和行为,不用在全部行为处理上都狂写switch/case。程序员

熟悉状态机Pattern的开发者不难想像出知足上述要求的代码结构;Component是稳定的,它即便一个子状态机的容器,又是一个行为的Proxy层,向this.state下的子状态机(例如命名为this.state.stm1)分发行为;逻辑上是下图所示:闭包

React Component

  this.state {
    stm1: // --------------------------------> stm1对象
  }

  this.handleToggleButton() {
    this.state.stm1.handleToggleButton() // -> stm1.handleToggleButton()
  }

同时分发的行为必须返回一个新的状态机对象用于替代旧的,它可能致使一次状态迁移,例如方法调用以前this.state.stm1是一个ListViewState对象,而调用后变成了ListEditState对象;若是是这样,上述行为方法得加一个逻辑:函数

this.handleToggleButton() {
    let newStm1 = this.state.stm1.handleToggleButton()
    if (newStm1)
      this.setState({ stm1: newStm1 })
  }

这个逻辑会反复使用,咱们不妨把它抽象出来性能

this.dispatch = (name, method, ...args) => {
    if (this.state[name] &&
      typeof this.state[name] === 'object' &&
      typeof this.state[name][method] === 'function') {
      let next = this.state[name][method](...args)
      if (next) {
        let obj = {}
        obj[name] = next
        this.setState(obj)
      }
    }
  }

这样在控件的JSX代码中使用时:ui

onToggle={e => this.dispatch('stm1', 'handleToggleButton')}

这不是惟一的写法,也许你不喜欢这样把全部的fallback都处理掉连错误通知也没有;你能够本身添加,写成本身喜欢的方式。this

Immutable State Machine in JavaScript

剩下的问题回到如何在JS下书写一个immutable的状态机问题,基于Class仍然是直觉的方式,不一样之处在于状态迁移时是用旧的Class对象做为参数传递给新的Class对象,新对象的构造函数第一件事情是复制旧对象的所有自有属性,这个行为能够写在原型类的构造函数里。

较为简洁的写法是状态机本身实现一个setState方法(setState是状态机Pattern的iconic方法,其次才是entry/exit);该方法只是用于状态机本身的状态迁移,和它的容器对象(React Component对象)上的setState方法无关;不要搞混了。(固然你应该想一想为何React Component上有这个状态机Pattern里的标志性方法)

简明实现的关键点是setState接受两个参数,第一个是下一状态的Class名(即构造函数),第二个是...args用于传参;全部子状态机的constructor都是(obj, ...args)的形式,obj是上一状态机;这样写能够避免实现setState时写switch/case。

它的简单实现能够是:

setState(NextState, ...args) {
  // 当前状态机迁出
  this.exit()
  // 构造新对象,immutable,同时下一状态机迁入,
  return new NextState(this, ...args)
}

原型类的构造函数能够看起来这样:

constructor(obj) {
  Object.assign(this, obj)
}

用于复制上一状态的全部属性。

最后这个状态机的基类须要一个exit方法,若是子类不须要实现,这是个fallback。

综上所述这个基类看起来大概是这样:

class STM {

  constructor(obj) {
    Object.assign(this, obj)
  }

  setState(NextState, ...args) {
    this.exit()
    return new NextState(this, ...args)
  }

  exit() {}
}

在实际使用的时候你可能须要本身的基类,由于

  • 你须要一些context,对全部状态都须要的值、属性、资源等

  • 你须要一些共同的方法,若是对某个行为的处理大部分状态都是同样的,那么能够写在这个原型类里,具体某个状态的行为不一样,它能够去重载;因此一个真正的原型类和继承类多是这样的:

class MySTM extends STM {

  constructor(obj) {
    super(obj)
  }

  this.handleToggleButton = () => {
    // ...
  }
}

class MySTMInitState extends MySTM {
  // ...
}

class MySTMAnotherState extends MySTM {
  // ...
}

须要注意的是不要在MySTM的构造函数里写其余逻辑,若是有其余逻辑,写在React Component的constructor里,至关因而这个状态机原型对象的工厂。

在React Component的构造函数里,能够这样使用:

// 若是props和进入时的上下文有关,在这里处理
  let props = {
    ...
  }

  // 建立了一个原型
  let stm1 = new MySTMInitState(props)

这里有两个问题须要阐述一下。

第一,基于class语法构造对象的本质,其实只是在子类构造函数里把父类构造函数所有调一遍,保证对象属性完整,以及原型链正确;它是用起来最简洁的方式,但不是惟一的方式;

JavaScript提供了另外一种方式来构造对象,即Object.create()方法,二者是有区别的。

基于class语法构造的对象,若是你尝试:

let x = new MySTMInitState({})
let y = new MySTMAnotherState({})

console.log(x.__proto__ === y.__proto__)

你会获得一个false输出,即这两个状态机的原型对象并不是同一个对象,他们只是同一个构造函数(MySTM)构造过,所以具备一样的properties(方法)。

可是若是你使用Object.create()来本身构造原型链,你能够有一个原型对象和React Component的生命周期一致,全部stm1状态机都以它为原型。这在某些状况下是有益的,例如:

  1. 你能够在这个原型上放context,减小迁移时Object.assign()复制properties的性能负担;

  2. 若是某些context是须要被子类修改的,能够提供setter方法达到这个目的。

事实上,这个方式更加符合JavaScript的原型化继承的设计初衷,可是语言是这样的一个东西,就是哪一个语法简单,那个写法就被最普遍的使用,就像C++/Java里继承是最简单的语法,那么它就被用的最普遍,而写Pattern是复杂实现,他就被用的少,即便不少时候更应该写Pattern。

Anyway,这个区别在实践上的意义很小。

第二,是个对传统OO语言开发者来讲比较难接受的地方,就是你能够这样写:

let x = new MySTM()
let y = new MySTMInitState(x)

这件事情幽默的地方是你能够用基类对象去构造继承类对象,仿佛Class和Object的区别被抹平的,他们在平行世界之间穿越。

其实这正解释了JavaScript的所谓类,只是构造函数,所谓继承,就是把构造函数和原型对象串起来而已,相似Builder Pattern的思想;因此Build两步仍是三步都是可能的。

这样写有一点实践上的意义,你能够先建立一个基类对象初始化全部的上下文,而后根据实际状况用它来构造继承类对象,这样能重用一下继承类对象的enter逻辑(即constructor),不用重写。

OK,这两个都是小问题,细节。move on。

在全部子类中,constructor等价于状态机Pattern的enter,用于建立全部资源,而exit中须要销毁全部资源,尤为是那些出发但还没有完成的请求,以及还没有fire的timer。对付这种问题,状态机是第一首选Pattern,简直太容易写出行为复杂且健壮的代码了。

事实上,任何其余形态的维护态的代码均可以看做是状态机Pattern的退化,因此对那些若是一开始就预见到将来会变得复杂的组件,应该一开始就写状态机;状态机牺牲的是代码量,可是对于行为定义的变化(迁移路径的增长,减小,改变,状态增减),它维护起来是无出其右的,是对付复杂多态行为的首选。

本质上,状态机帮你拿掉在全部方法里的第一层switch/case,代之以dispatch,或者是OO里说的多态;可是若是状态层叠呢?

一般咱们不在状态机里套状态机,通常只有在写复杂协议栈的时候这么写;通常而言,状态机两层最多了,内层的状态用值来表示状态,而不是用类来表示状态,足够了。

举个例子看看你理解了没有:

你的UI里有一个行为是操做一个列表中的单一对象;若是有一个对象被选中,而后按钮被点击,这是一种行为,另外一种是用户先建立一个新对象,这是另外一种行为;那么须要把Editing和EditingNew做为两种互斥状态处理吗?

若是没有UI的颠覆性变化大多数状况不这样作,而是把Editing做为顶层状态机(superstate)处理,而New能够用一个props的值来表示,例如状态机对象里有一个叫作creating的prop,它是boolean类型。即顶层状态机用类对象表示,底层状态机回到土办法,用值表示。

这样设计的好处是:

  1. Editing和EditingNew有大量状态是重用的和persistent的,即从一个迁移到另外一个,他们仍然是有效的,不该该被一个exit销毁,另外一个enter重建。

  2. 他们做为父子状态设计能够共用大量方法,而不是每一个都提供本身的副本;

  3. 若是从父状态迁出或者从外部状态向父状态迁入,销毁和构建资源的逻辑也大部分是相同的;

实际上的状态图上每每是有superstate(父状态)迁出的事件逻辑;那么执行方式是

  1. 直接调用父状态的exit

  2. 父状态的exit先dispatch子状态的exit

  3. 父状态的exit再调用本身的逻辑,即清理子状态的共享资源。

若是是外部迁入父状态机,要有一个决策依据决定应该迁向那个子状态机做为初始状态,由于在runtime,组合状态机构成的tree结构,实际的状态机实例只能在leaf node上,superstate节点的存在是为了抽象子节点的共同行为,减小迁移路径和重用行为逻辑;

所以迁入父状态机时(enter)的逻辑和迁出(exit)恰好相反:

  1. 直接调用父状态机的enter

  2. 父状态机先构造对全部子状态都适用的资源

  3. 调用具体某个子状态机的enter(就是一个if / then来区分子状态机便可)

在OO领域,不少开发者信奉UML图;UML图对OO语言中最重要的类图,在JavaScript里毛用没有了,可是State Machine图,结合上述状态机设计,绝对是对付复杂UI的利器;尤为是对于初学者而言,在前端的状态逻辑上,你能掌握这一把刀就能砍倒全部的树;若是还不能砍倒,那其实问题自己不是UI构建域的,多是其余问题,例如调度等等。

不少写JavaScript的朋友,为了向世人证实本身根骨奇佳、习得真传,处处宣扬OO里的种种不是,以各类言辞抨击OO实践的方方面面。

他们不懂OO。

OO里在语言层面可能有一些设计问题,可是OO里的封装思想是绝对正确的;

为何会有对象这个概念被提出来?就是由于一些态的生命周期超过函数调用的执行时间,你须要一种方式来管理这些态。

封装的本质是:在内部有一个state space,在外部看,只看到内部的state space的superstate。物理学上称之为简并,degeneration。

这是咱们对付全部复杂状态的惟一手段,无论态放在花盆里、银行里、仍是藏在本身的内裤里,他们都是客观存在,你不可能去消灭态,你只能organize他们;并且你同时须要organize应用在态上过程(function)。

状态机把这个organization完彻底全一览无遗的展露出来,不管你用class写,用闭包写,用c语言写,行为和状态的structure都不会变,想成为一个合格的程序员,尤为是写ui的程序员,state machine pattern是必修课。

~~~~~~~~~~~~~~~~~~~

先写这么多,我得按照上述逻辑扣代码去了。

祝你们圣诞节快乐。

欢迎探讨。

相关文章
相关标签/搜索