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
剩下的问题回到如何在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状态机都以它为原型。这在某些状况下是有益的,例如:
你能够在这个原型上放context,减小迁移时Object.assign()
复制properties的性能负担;
若是某些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类型。即顶层状态机用类对象表示,底层状态机回到土办法,用值表示。
这样设计的好处是:
Editing和EditingNew有大量状态是重用的和persistent的,即从一个迁移到另外一个,他们仍然是有效的,不该该被一个exit销毁,另外一个enter重建。
他们做为父子状态设计能够共用大量方法,而不是每一个都提供本身的副本;
若是从父状态迁出或者从外部状态向父状态迁入,销毁和构建资源的逻辑也大部分是相同的;
实际上的状态图上每每是有superstate(父状态)迁出的事件逻辑;那么执行方式是
直接调用父状态的exit
父状态的exit先dispatch子状态的exit
父状态的exit再调用本身的逻辑,即清理子状态的共享资源。
若是是外部迁入父状态机,要有一个决策依据决定应该迁向那个子状态机做为初始状态,由于在runtime,组合状态机构成的tree结构,实际的状态机实例只能在leaf node上,superstate节点的存在是为了抽象子节点的共同行为,减小迁移路径和重用行为逻辑;
所以迁入父状态机时(enter)的逻辑和迁出(exit)恰好相反:
直接调用父状态机的enter
父状态机先构造对全部子状态都适用的资源
调用具体某个子状态机的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是必修课。
~~~~~~~~~~~~~~~~~~~
先写这么多,我得按照上述逻辑扣代码去了。
祝你们圣诞节快乐。
欢迎探讨。