State Machine in React Component

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

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

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

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

解决这个问题并不难,行为binding使用Component对象上的方法,它是稳定的,不会因为model的状态机更迭而变化,但它是一个proxy,需要把方法分发到子状态机上;这样我们就得到了状态机Pattern的最大优势:每个状态只关注属于自己的子状态,值,资源,和行为,不用在所有行为处理上都狂写switch/case。

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

React Componentthis.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] = nextthis.setState(obj)}}}

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

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

这不是唯一的写法,也许你不喜欢这样把所有的fallback都处理掉连错误通知也没有;你可以自己添加,写成自己喜欢的方式。

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是必修课。

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

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

祝大家圣诞节快乐。

欢迎探讨。

React Component里的状态机Pattern相关推荐

  1. React Component vs React Element

    React Component vs React Element 有这样的一个问题: // 方法定义 function add(x, y) {return x + y }// 方法调用 add(1, ...

  2. [译]React Component最佳实践

    原文:Our Best Practices for Writing React Components . 这里意译.有些点在之前的文章里提到过:#2 译文地址:https://github.com/Y ...

  3. React.Component

    React.Component 本章节提供了 React class 组件的详细 API 参考.本章节默认你已熟悉基本的 React 概念,例如 组件 & Props,以及 State &am ...

  4. 六、React(Component)

    文章目录 前言 一.component基础 二.Component的组合运用 总结 前言 本文介绍react的重中之重,react的组件.组件是react的核心所在,第一部分先介绍组件的基础内容,并且 ...

  5. 在React Hook里使用history.push跳转

    在React Hook里使用history.push跳转 react hook里用不了this.props.history的解决方法 首先引入 import { useHistory } from ' ...

  6. 转载 React.createClass 对决 extends React.Component

    先给出结论,这其实是殊途同归的两种方式.过去我们一般都会使用 React.createClass 方法来创建组件,但基于 ES6 的小小语法糖,我们还可以通过 extends React.Compon ...

  7. SAP Spartacus List Component 里定义的待显示 Popover 内容,如何传递到 Popover Component 里的?

    中心思想:这个传递以 Popover Directive 为桥梁. PopoverComponent里的 content 属性: string 或者 TemplateRef,后者是 ng-templa ...

  8. 如何在SAP Spartacus自定义Component里消费数据

    直接在自定义Component里使用data来访问Component数据: <p>begin of my image!</p> <img [src]="'htt ...

  9. React.Component(V16.8.6)

    组件的生命周期 挂载 当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下: constructor() static getDerivedStateFromProps() render() ...

  10. React.Component与React.PureComponent的区别

    转发自:https://blog.csdn.net/qq_29854831/article/details/79657718 React.PureComponent 与 React.Component ...

最新文章

  1. numpy中的tile函数
  2. 一个DBA的工作写照
  3. 创新正当时 -「Innovation 2021」网易应用创新开发者大赛决赛十强正式集结
  4. 16年华科计算机分数线,2016年华中科技大学艺术类专业录取分数线
  5. RPC、RMI与MOM与组播 通信原理 .
  6. the sdk seems invalid._直播平台中必不可少的美颜SDK
  7. 在视觉任务上大幅超越ReLU的新型激活函数
  8. 最大概率法分词及性能測试
  9. python之类与对象(2)
  10. Tomcat服务器报错IOException: Broken pipe
  11. ZK在Eclipse下的环境搭建
  12. Android context.getSystemService的简单说明
  13. C#中导出电子表格Execl总结
  14. python3安装及加载gensim
  15. linux运行崩溃怎么定位,Linux 程序崩溃定位
  16. 如何实现网站访问量统计(html页面)
  17. 如何解决运行Spark-shell,出现报错Unable to load native-hadoop library for your platform的问题
  18. 新南威尔士大学计算机博士申请,unsw大学研究生
  19. read write file
  20. Choregraphe中NAO机器人如何使用纯python指令盒让机器人说话

热门文章

  1. django mysql处理_利用Django去操作数据库并完成简易的登录及编辑功能
  2. fastreport按条件查询_查询代价的
  3. 抖音自动清关注服务器,各位有保留抖音自动关注的代码吗
  4. Android踩内存工具,Android之内存分析工具
  5. c语言链表插入尾部,为什么我的程序一执行插入链表尾部,再执行别的操作就会出现问题,...
  6. c语言链表的数据结构,c语言实现通用数据结构(一):通用链表
  7. ajax 循环php数组,jQuery通过ajax请求php遍历json数组到table中的代码(推荐)
  8. mysql读写分离_SpringBoot+MyBatis+MySQL读写分离
  9. 小米这样做,我是拒绝的
  10. Mininet实验 自定义拓扑结构