状态机
一般指的是有限状态机(Finite State Machine,FSM),又可以称为有限状态自动机(Finite State Automation,FSA),简称状态机,它是一个数学模型,表示有限个状态以及在这些状态之间的转移和动作; 无限状态机类似永动机,属于只有理论意义但不存在的模型。
六要素:起始、终止、现态、次态(目标状态)、动作、条件。
现态:是指当前所处的状态
条件:又称“事件”,当一个条件被满足,将触发一个动作,或者执行一次状态迁移
动作:条件满足后执行的动作
次态:条件满足后迁往的新状态
要点:
1、避免程序动作当做一种状态处理
动作是不稳定的,一旦执行完毕就结束了。
状态是相对稳定的,没有外部条件的触发,一个状态会持续下去
2.状态划分漏掉的一些状态
XState状态机使用
安装:npm install xstate
import { interpret, createMachine, assign, State } from 'xstate';
const globalContext = convertToMap(this.m.options ? this.m.options.variables : []); // 全局context 初始化
globalContext['m'] = { name: this.m.name, title: this.m.title };
let xstateContextKey = `${redisBus.xstateContext}.${this.m.name}`; //redis缓存context的key
let xstateInitialKey = `${redisBus.xstateInitial}.${this.m.name}`;//redis缓存initial的key
/** startAll方法将schema转换为xstate格式,并创建machine
* schema //状态机配置数据
* this.ruleEngines //规则引擎配置, 这里用的是json-ruler-engine 和 amis的condition-builder俩种方式。
* this.logicComposerMap //逻辑引擎配置,自行实现的。
* this.OPCMap // 传感器数据: { xxx设备: 1.5, xxx设备: 2.4 }
*/
this.services = await xstateServices.startAll(this.m.schema, globalContext, this.ruleEngines, this.logicComposerMap, xstateContextKey, xstateInitialKey, this.OPCMap);
for (let service of this.services) {
await service.start(); //启动状态机
if (service.machine.config.on["goto_lastState"]) service.send("goto_lastState");//跳转最后状态事件
service.subscribe((state) => { //监听状态改变
let context = JSON.stringify(state.context); //此machine context + globalContext
// redis.set(`${xstateContextKey}.${service.id}`, context); //保存状态机上下文
let xIniID = Object.keys(state.meta)[0]; //state.meta key 状态节点 id
let xIniValue = Object.keys(state.value)[0]; //value状态名
console.log(xIniID + '----' + xIniValue, service.id);
// redis.set(`${xstateInitialKey}.${service.id}`, xIniID); //保存最后状态
});
}
async startAll(schema, globalContext, ruleEngines, logicComposerMap, xstateContextKey, xstateInitialKey, OPCMap) {
//。。。省略
let deviceServices = [];
let machines = await this.getMachines(schema, globalContext); //转换成xstate要求的结构
for (let machine of machines) {
let deviceMachine = createMachine(machine.rootState, machine.config);
let deviceService = interpret(deviceMachine, {devTools:true});
deviceServices.push(deviceService);
}
return deviceServices;
}
额外:json-rules-engine规则引擎的基本用法
const { Engine } = require('json-rules-engine')
const engine = new Engine([], { allowUndefinedFacts: true });//allowUndefinedFacts: true将undefined变为`undefined` 而不是抛出异常
//添加判断规则
engine.addRule({
conditions: {
all: [{ fact: 'gameDuration', operator: 'equal', value: 48}, {fact: 'personalFoulCount', operator: 'greaterThanInclusive', value: 6 }]
},
event: { // 定义条件评估为真时要触发的事件
type: 'fouledOut',
params: { message: 'Player has fouled out!' }
}
});
let facts = { //需要判断的数据
personalFoulCount: 6,
gameDuration: 40
}
// 运行engine进行规则判断
engine.run(facts).then(({ events }) => {
console.log(events)
});
ps:若addRule添加多个rule都匹配之后得到的是events数组:
[
{ type: 'guard', params: { name: 'sadasd', value: true } },
{ type: 'fouledOut', params: { message: 'Player has fouled out!' } }
]
`allowUndefinedFacts`参数 -默认情况下,当运行中的引擎遇到undefined的fact时,会抛出异常
启用此选项将导致引擎将undefined的fact视为“undefined”。 (default: false)
以状态机的思维设计功能逻辑:
以设计登录功能的状态图为例:
要为登录功能绘制状态图,首先列出流程中的基本事件。想想你的登录过程会做什么:
登录
注销
然后列出作为这些事件结果而存在的状态:
登录
注销
一旦有了一些事件和状态,状态图就出来了。
登录功能状态图显示初始状态:注销状态。通过登录事件转换为登录状态,然后通过注销事件转换回注销状态。
在这里,注销状态是初始状态,因为任何新用户都会进入注销的进程。
xstate相关api介绍
本文是基于xstate4.0的相关api说明
官方文档: https://stately.ai/docs/quick-start
Xstate提供了状态扭转所需要的所有基本步骤,使用配套的可视化工具可以快速搭建一套包含各个业务节点的工作流
形式上,有限状态机有五个部分:
有限数量的状态
有限数量的事件
初始状态
在给定当前状态和事件的情况下确定下一个状态的转换函数
最终状态集(可能为空)
状态是指由状态机建模的系统的一些有限的、定性的“模式”或“状态”,并不描述与该系统相关的所有(可能是无限的)数据。例如,水可以处于以下四种状态之一:冰、液体、气体或等离子体。然而,水的温度可以变化,它的测量是定量的和无限的。
actor模型里所有东西都是一个“actor”,可以做三件事:
接收消息
向其他Actor发送消息
对它接收到的消息(它的行为)做一些事情,例如:
改变其本地状态
向其他Actor发送消息
衍生新Actor
Actor的行为可以由状态机(或状态图)描述。
machine 状态机
创建machine:
建议在根状态节点(机器)上使用id。不同machine是不同的Actor不会相互影响。
const lightMachine = createMachine(
{ //参数一: rootState
id: 'light',
initial: 'green',
context: { },
states: {
green: {
entry: ['alertGreen', 'action2'], //进入时触发action,
exit: ['alertGreen', 'action2'], //离开时触发action,
on:{
'event.on': [ //监听到event.on事件,顺序执行转换,cond为true则执行actions动作同时转换状态,转换状态后不再顺序执行转换。
//cond都不满足,则将事件传递到父状态上。
{ cond: isAdult, target: 'adult', actions:[ 'action1' ] }
]
},
after: [//延迟执行。 (不需要事件触发) 与on类似,将采用第一个cond为true且延迟时间完成的延迟转换,这将防止其他的延迟转换执行。如下LIGHT_DELAY满足,但500优先触发执行转换,阻止了LIGHT_DELAY执行.
// cond都不满足则不再触发。
{
delay: 'LIGHT_DELAY', //通过函数返回延迟时间。也可以直接delay: 1000
cond: isAdult, target: 'yellow', actions:[ 'action1' ]
},
{ delay: 500, cond: ()=>true, target: 'red'}
],
//也可以这样配置:after: {
LIGHT_DELAY: { cond: isAdult, target: 'yellow'}
1000 : { cond: ()=> true, target: 'red'}
}
always: [ //always 无事件转换,进入状态时就发生(不需要事件触发) 与on一样,采用第一个cond为true的转换,这将防止其他转换的执行。
// cond都不满足则不再触发。
{ target: 'win', cond: 'didPlayerWin',actions:[ 'action1' ] }
],
"invoke": [
{
// `src` 属性可以是:
// - a string in machine options.services
// - a machine
// - a function that returns...
src: (context, event) => {
// - a promise
// - a callback handler
// - an observable
},
id: 'some-id',
//(可选)将状态机事件转发到被调用的服务(目前仅适用于状态机!)
autoForward: true,
//(可选)调用的promise/observable/machine完成时的转换
onDone: [ { target: /* ... */ } ],
// (可选)当被调用的服务发生错误时的转换
onError: [ { target: /* ... */ } ]
}
]
}
}
},
{ //参数二:config全局配置
actions: { //动作 entry/exit/actions配置使用
// action implementation
alertGreen: (context, event) => {
alert('Green!');
}
},
delays: { //after中delay配置使用
LIGHT_DELAY: (context, event) => { //返回延迟时间
return context.trafficLevel === 'low' ? 1000 : 3000;
},
},
guards: { //守卫条件,cond配置使用: cond: 'serchVaild'
searchValid: (context, event) => {
return context.canSearch && event.query && event.query.length > 0;
}
}
services: { //调用的服务 (invoke.src) service异步
asyncGuard1: (context, event) => {
await (async(context, event) =>{//invoke支持Promise/callback/machine/Observable
return true;
})(context, event)
}
}
});
.withConfig()进行扩展配置:
const noAlertLightMachine = lightMachine.withConfig({
actions: {
alertGreen: (context, event) => {
console.log('green');
}
}});
withContext修改machine的context,会直接覆盖。
const testLightMachine = lightMachine.withContext({
// merge with original context
...lightMachine.context,
elapsed: 1000});
state 状态
State对象实例是可json序列化的,并具有以下属性:
value -当前状态值(例如,{red: 'walk'})
context—此状态的当前上下文
event——触发转换到此状态的事件对象
actions——要执行的动作的数组
activities——如果活动启动,则将活动映射为true;如果活动停止,则将活动映射为false。
history -前一个State实例
meta -在状态节点的Meta属性上定义的任何静态元数据
done—状态是否指示最终状态
State对象还包含其他属性,如historyValue、事件、树和其他通常不相关且在内部使用的属性。
State id:
元素的id在当前会话必须是唯一的,如果id属性是可选的并且被忽略,除了<send>和<invoke>外,处理器必须在加载文档时生成一个唯一的id(这些生成的id通常不能在文档的其它地方引用,因为文档作者无法预先知道它们的值。如果一个状态采用系统生成的id,就无法作为转移的目标)。<send>和<invoke>的id不太一样。它们必须在会话中是唯一的,但如果在文档中没有设置,处理器不是在文档加载时产生id,而必须在每次执行时产生id。这时,可以使用idlocation获取这些自动生成的id。
默认情况下,状态节点的id是带分隔符的完整路径。你可以使用这个默认id来指定一个状态节点。
复合state/根state的initial只允许选择此states下(第一层子状态)的状态名。比如a,不能通过a.b的方式跨复合节点选取。
相对Targets:
子状态节点可以通过指定一个点('.')后跟它们的键来相对于它们的父状态节点进行定位:
默认情况下,相对目标是内部转换,这意味着父状态不会退出并重新进入。你可以通过指定internal: false使相对目标外部转(外部转换,将退出/重新进入父状态节点)
自定义id(& 绝对Targets)
状态节点可以通过唯一标识符而不是相对标识符来定位。这可以简化复杂状态图的创建。
为一个状态节点指定一个ID,提供一个唯一的字符串标识符作为它的ID属性,例如,ID: 'greenLight'。
要通过ID定位一个状态节点,在它的字符串ID前加上'#'符号,例如TIMER: '#greenLight'。
例如:
const lightMachine = createMachine({
id: 'light',
initial: 'green',
states: {
green: {
// 未指定自动生成default ID: 'light.green'
on: {
TIMER: { target: '#light.yellow' } // 绝对target通过ID访问,
}
},
yellow: {
on: { //访问子节点: 'red.A1' 或 '#light.red.A1'
TIMER: { target: 'red' } //相对Target 等价于 '.red' '#light.red'
}
},
red:{ on:{ } }
}
});
state.meta
state的meta是 处理活跃状态的所有state的meta集合。
原子state的meta是此state的meta
复合state的meta是处于活跃状态的子state和父state的meta集合(父state一直处于活跃状态)
平行state是组成此平行state的所有state的meta集合。(所有state都处于活跃状态)
常用方法:
state.matches('red') //=> true 判断状态值是否是red
arr.some()方法用于检测数组中的元素是否满足指定条件(函数),有一个满足即为true。
arr.every( (item)=>{ return item === true; } ) //arr所有元素都满足才为true
const isMatch = [{ customer: 'deposit' }, { customer: 'withdrawal' }].some(state.matches);
console.log(state.done) //=>true 判断是否为最终状态
console.log(initialState.nextEvents);//=> ['TIMER', 'EMERGENCY']。nextEvents指定将导致从当前状态转换的下一个事件。
状态节点可以有标记,这些标记是帮助描述状态节点的字符串术语。标签是元数据,可用于对不同状态节点进行分类。例如,你可以使用"loading"标记来表示哪些状态节点代表数据正在被加载的状态,并使用state.hastag (tag)确定状态是否包含这些标记的状态节点:
loadingUser: {
tags: ['loading']
},
state类型
1.initial state 初始状态:
任何有状态的进程都会有一个初始状态,即进程存在的默认状态,直到发生事件改变进程的状态。初始状态由一个填满的圆表示,圆上有一个箭头指向初始状态。
2.final state最终状态:
大多数有状态的流程都有一个最终状态,即流程结束时的最后一个状态。最终状态由状态的圆角矩形框上的双边框表示。
在复合状态下,到达一个final子状态节点(使用{type: 'final'}),将在内部为该复合状态节点引发一个done(…)事件。使用onDone监听done事件并处理。
当并行状态节点中的每个子状态节点都完成时,父并行状态节点也完成。当到达每个子复合节点中的每个最终状态节点时,并行状态节点将引发done(…)事件。
3.atomic state 原子状态:
原子状态是没有任何子状态的状态。
复合state/根state的initial只允许选择此states下(第一层子状态)的状态名。比如a,不能通过a.b的方式跨复合节点选取。
4.compound state 复合状态:
复合状态是可以包含更多状态的状态,也称为子状态。这些子状态只能在父复合状态发生时发生,不会同时发生。在行走状态中,可能有行走、奔跑和停下来闻气味的子状态。
复合状态由一个有标记的圆角矩形框表示,该矩形框充当子状态的容器。
复合状态还应该指定哪个子状态是初始状态。在行走复合状态下,初始状态是行走。
复合状态使状态图能够处理比日常状态机更复杂的问题。
对于复合状态而言:若当前状态不处理 event 时,该 event 将传播到其要处理的父状态。如果状态或其任何祖先(父)状态均未处理事件,则不会发生转换。 在 strict 模式下(在 状态机配置options 中指定),这将引发错误。
5.parallel state平行状态:
并行状态是一种复合状态,其中它的所有子状态(也称为区域)同时处于活动状态。这些区域在复合状态容器内由虚线分隔。
在on a walk复合状态中,可能有两个区域。一个区域包含狗的活动子状态(走、跑和停下来闻好气味),另一个区域包含狗的尾巴状态(摇和不摇)。狗可以走,摇尾巴,跑,摇尾巴或停下来,一边摇尾巴一边嗅,它也可以不摇尾巴就做这些活动。
这两个区域还应该指定哪个子状态是初始状态。
6.history 历史状态
历史状态节点 是一种特殊的状态节点,当它到达时,告诉状态机转到该区域的最后一个状态值。 有两种类型的历史状态:'shallow' 'deep'
指定了 target 后,如果定义历史状态的 state 区域不存在历史,则默认情况下它将进入 target 状态:
event & transition
事件是导致状态机从当前状态转换到下一个状态的原因。
事件是一个带有type属性的对象,表示它是什么类型的事件:
在XState中,只有一个类型的事件可以用它们的字符串类型来表示,作为一种简写:'TIMER'等价于{ type: 'TIMER' }
空事件是没有类型的事件,一旦进入状态就立即发生(空事件语法({on:{":…}})将在版本5中被弃用。应该使用新的always语法)。空事件有很多用例,特别是在定义瞬态转换时。
转换(transition)是由导致状态变化的事件(event)引起的。转换在on监听事件中触发
转换和事件是确定的。确定性意味着每次流程运行时,每个转换和事件总是指向相同的下一个状态,并且总是从给定的起始条件产生相同的结果。
1.Self-transition自转换
自转换是指发生了一个事件,但是转换返回到相同的状态。转换箭头退出并重新进入相同的状态。描述自我转变的一个有用的方法是在这个过程中“做某事,而不是去某个地方”。
在狗乞讨过程中,会有一个带有gets treat事件的乞讨状态。对于爱吃的狗狗来说,无论你经历多少次“得到款待”事件,狗狗还是会回到乞讨的状态。
EVENT: { target: '. foo' } - 到子节点的内部转换(以'.'开头)
EVENT: undefined - 禁止转换
EVENT: { actions: [ ... ] } - 内部的自我转换
EVENT: { actions: [ ... ], internal: true } - 同上,内部的自我转换
EVENT: { target: undefined, actions: [ ... ] } -同上,内部的自我转换
2;Delayed transition 延迟转换
延迟转换是一种转换,这种转换发生在处于特定状态一段时间之后。延迟的转换被标记为“after”和一个固定的持续时间,以指示在转换到下一个指定的状态之前应该经过多少时间。
延迟转换不需要事件触发,与on一样,将采用第一个cond为true且延迟时间完成的延迟转换,这将防止其他的延迟转换执行。
transition到'two': on: { CLICK: 'two' } /on: { target: 'two', cond: ()=>{ return true; } }
转换被启用的条件:
1.当前状态中定义了转换的事件被触发时
2.转换守护条件(cond属性)被满足(计算为true)
3.它没有被更具体的transition所取代。
额外:转换的优先级取决于它们在树中的深度;更深层次的转换更具体,因此具有更高的优先级。这与DOM事件的工作方式类似:如果单击按钮,则按钮上的click事件处理程序比窗口上的click事件处理程序更优先。
machine.transition转换方法:
state—要过渡的状态
event——导致转换的事件
它返回一个新的State实例,该实例是接受当前状态和启用的事件的所有转换的结果。
事件监听on & 触发案例:
const skipMachine = createMachine({
id: 'skip',
initial: 'one',
states: {
one: {
on: { CLICK: 'two' } // 等价于{ target: 'two', cond: ()=>{ return true; } }
},
two: {
on: { '': 'three' }//空事件是在进入状态时发生,立即转换到'three'
always: [{target: 'three'}], // 空事件等价于always(空事件即将废弃, 推荐always)
on: { 'event.on': [ //on监听触发多组行为时,顺序执行cond判断,若为true执行动作同时触发状态转换,状态转换之后,后面的行为就不再执行了,未发生转换则继续顺序执行。
{ target: 'adult', cond: isAdult },
{ target: 'child', cond: isMinor }
]
}
},
three: {
type: 'final'
}
}
});
const { initialState } = skipMachine;
const nextState = skipMachine.transition(initialState, 'CLICK'); //通过CLICK事件转换
console.log(nextState.value); // => 'three'
监听window本身事件:
const mouseMachine = createMachine({
on: {
mousemove: {
actions: [ //触发动作
(context, event) => {
const { offsetX, offsetY } = event;
console.log({ offsetX, offsetY });
}
]
}
}
});
const mouseService = interpret(mouseMachine).start();
window.addEventListener('mousemove', (event) => {
mouseService.send(event);// event can be sent directly to service
});
guard
guard守卫条件返回bool类型。
如果cond guard返回false,则不会转换状态,也不会执行action。如果子状态中的所有转换的守卫条件都为false不进行转换,则事件将被传播到父状态并在那里处理。
start: {
on: {
NUMBER: [
{
cond: "isZero", //cond返回true,执行action并转移状态
target: "operand1.zero",
actions: ["defaultReadout"]
},
}
}
异步 guard
xstate中guard只能是同步的,不支持异步guard。但是可以通过invoke模拟异步guard
使用状态机和状态图,所有状态(包括中间状态)都必须是显式的。所以不要:
你可以用某种调用异步守卫的中间“检查”状态来建模:
它可能看起来像这样:
CHECKING: {
invoke: {
src: 'asyncGuard',
onDone: 'B',
onError: 'A'
}
}
interpreter (service)
虽然带有machine.transition()函数的状态机/状态图在灵活性、纯度和可测试性方面很有用,但为了让它在现实应用程序中有任何用处,需要:
跟踪当前状态,并持久化它
处理延迟的转换和事件
与外部服务通信
interpreter解释器负责解释状态机/状态图并完成上述所有工作——也就是说,在运行时环境中解析并执行它。状态图的解释的、正在运行的实例称为服务service。
例如:
const machine = createMachine(/* machine config */);
// Interpret the machine, and add a listener for whenever a transition occurs.
const service = interpret(machine).onTransition((state) => {
console.log(state.value);
});
service.start();// Start the service
service.send({ type: 'SOME_EVENT' });// Send events
service.stop();// Stop the service when you are no longer using it.
service.send({ type: 'CLICK', x: 40, y: 21 }) //作为event参数传递,获取event.x
service.send('CLICK', { x: 40, y: 21 })) //作为payload对象 4.5+支持
service.send(eventType, {to : 'ponger'}) //目标actor只能指定本machine下的actor(包括子Actor)。若是其他machine的actor报错如下:Unable to send event to child 'ponger' from service 'pinger'
subscribe监听状态的改变:
服务(从interpret(machine)创建)可以通过.subscribe(…)方法订阅。订阅将收到所有状态更改(包括初始状态)的通知(machine根节点的initial通知一次,之后每次send触发transation后通知一次),并且可以取消订阅。
machine.start();
machine.subscribe((state) => { //staet为转换的target节点。
console.log(state);
});
默认情况下,当状态转换时立即执行action。这可以通过设置{execute: false}选项来配置不自动执行。在状态上指定的每个操作对象都可能有一个.exec属性,该属性与状态的上下文和事件对象一起调用。
可以通过调用service.execute(state)手动执行操作。当你想控制动作何时执行时,这很有用:
const service = interpret(machine, {
execute: false // do not execute actions on state transitions
});
service.onTransition((state) => {
// execute actions on next animation frame, instead of immediately
requestAnimationFrame(() => service.execute(state));
});
service.start();
waitFor:用于异步函数的最佳模式是waitFor,它使您能够等待处于特定状态的状态机。
const machine = createMachine({
initial: 'pending',
states: {
pending: {
after: {
3000: {
target: 'done'
}
}
},
done: {}
}
});
const myFunc = async () => {
const actor = interpret(machine).start();
const doneState = await waitFor(actor, (state) => state.matches('done'),{timeout: 20_000}); //超时时间20秒
console.log(doneState.value); // 'done'
};
activities活动 已废弃
活动是随时间发生的操作,可以启动和停止。根据 Harel 的原始状态图论文:
(活动总是花费非零时间,例如发出哔哔声、显示或执行冗长的计算)
当一个状态节点进入时,解释器应该开始它的活动,当它退出时,它应该停止它的活动。
注意:活动已弃用,并将在XState版本5中删除。推荐的方法是invoke调用一个callback类型的actor。
const toggleMachine = createMachine(
{
id: 'toggle',
initial: 'inactive',
states: {
active: {
// 只要状态机处于 'active' 状态, 'beeping' 活动就会发生
activities: ['beeping'],
on: {
TOGGLE: { target: 'inactive' }
}
}
}
},
{
activities: {
beeping: () => {
// 进入时开始 beeping activity
const interval = setInterval(() => console.log('BEEP!'), 1000);
// 返回一个函数,用于退出时停止 beeping activity
return () => clearInterval(interval);
}
}
}
);
Action & service
action & service区别:action只能调同步方法(entry/exit),service可以异步 Promise(由invoke调用,调用Actor)
Action动作
Action动作可以在进入或退出状态时触发。动作是“即发即忘效果”;一旦机器启动了动作,它就会继续前进,忘记这个动作。您还可以在转换上触发操作。
您可以对一个状态触发多个进入和退出操作。顶级最终状态不能有退出操作,因为机器已停止,不能发生进一步的转换。
entry 进入触发动作,exit离开触发动作,actions状态转换时触发动作:
const triggerMachine = createMachine(
{
id: 'trigger',
initial: 'inactive',
states: {
inactive: {
on: {
TRIGGER: {
target: 'active',
actions: [ 'sendTelemetry'] // transition actions
}
}
},
active: {
entry: [ 'sendTelemetry'], // entry actions
exit: [ 'sendTelemetry'], // exit actions
on: {
STOP: { target: 'inactive' }
}
}
}
},
{
actions: {
sendTelemetry: (context, event) => {// action implementations
console.log('time:', Date.now());
}
......
}
}
);
也可以在state中覆盖实现machine中的action:
TRIGGER: {
target: 'active',
actions: (context, event) => { console.log('activating...'); }
}
state实例有一个actions属性,里面是interpreter解释器要执行的action数组。
console.log(activeState.actions);
//[{ type: 'sendTelemetry', exec: funcion },{ type: 'activate', exec: funcion }]
动作创建者
以下所有函数均是动作创建者。它只返回一个 动作 对象,并 不会 命令式地发送一个事件,故无法在代码中调用触发,只能用在actions声明中使用。
例如:
entry/exit/actions: [raise('EVENT'), 'raiseEvent' ], //触发事件
machine.options:{
actions:{ raiseEvent: raise('EVENT') }
}
注意pure动作可以通过函数return一个action数组,指定要执行的动作。
entry: pure((context, event) => {
return [ raise('SOME_EVENT'), log("aaaaa") ]; //执行俩个动作 raise 和 log
})
sendTo/raise 动作 触发事件:
send(event) 动作创建器创建了一个特殊的“发送”动作对象,该对象告诉服务(即解释机器)将该事件发送给自己。它在外部事件队列中将事件排队到正在运行的服务中,这意味着事件将在解释器的下一个“步骤”中发送。
send(…)动作创建器已弃用,改用sendTo(…)和raise(...)动作创建器:
-send({type: 'EVENT'},{to:'someActor',delay:5}); //to指定触发的actor (默认是它自己),to只能指定它和它的子actor。
+sendTo('someActor', {type: 'EVENT'});
要向self发送事件,应该使用raise(…):
-send({type: 'EVENT'});
+raise({type: 'EVENT'});
send(…)动作创建器将在XState v5.0中被删除。
注意:send(...) 函数是一个动作创建者,不会命令式发送事件。 相反,它返回一个 动作 对象,描述事件将发送到的位置
raise() 动作 创建者在内部事件队列中,将一个事件排入状态图。 这意味着事件会在解释器(interpret)的当前“步骤”上立即发送。
actions: raise('NEXT')
sendParent('PONG', { delay: 1000 })
forwardTo转发动作
原理是创建一个 send() 动作,通过指定的 invokeID 将最近的事件转发到指定的服务
import { createMachine, forwardTo, interpret } from "xstate";
const parentMachine = createMachine({
id: "parent",
invoke: {
id: "alerter",
src: (context, event) => {
return function alertService(callback, onReceive) {
// console.log(context, event);
onReceive((event) => {
if (event.type === "ALERT") {
console.log(event.message);
}
});
};
},
},
on: {
ALERT: { actions: forwardTo("alerter") }, //转发传来的事件到alerter Actor中
},
});
const parentService = interpret(parentMachine).start();
parentService.send({ type: "ALERT", message: "hello world" });
assign动作 更改context
更新状态机的 context, 在XState中为上下文分配新值是通过assign动作完成的,这是改变机器上下文的唯一方法。永远不要从外部改变机器的上下文。,每个上下文更改都应该显式地由事件触发。
actions: {
assignToContext: assign((context) => { //更改context的值
return {
count: context.count + 1,
、 message: 'Count changed',
};
}),
},
choose动作 指定应根据某些条件执行哪些 动作
import { createMachine, actions } from "xstate";
const { log, choose } = actions;
const loggingMachine = createMachine(
{
id: "logging",
context: { count: 42 },
initial: "start",
states: {
start: {
entry: log("started!"),
on: {
FINISH: {
target: "end",
actions: choose([ //类似于 SCXML <if>、<elseif> 和 <else> 元素
{ cond: "cond1", actions: [log("成功")], }, //触发log打印 成功到控制台
{
cond: (context, event) => { return true; },
actions: [ log("cccccc"),
(context, event) => {
console.log("aaa");
//return log("bbb"); //不生效,log函数是log动作声明,不是函数式调用,必须放到actions下才起作用。
} ],
},
]),
},
},
},
end: {},
},
},
{
guards: {
cond1: () => { return true; },
},
}
);
const endState = loggingMachine.transition("start", "FINISH");
pure纯动作
根据触发 动作 的当前状态“上下文”和“事件”返回要执行的 动作 对象。 这允许你动态定义应执行哪些 动作
import { createMachine, actions } from 'xstate';
import { raise, log , pure } from 'xstate/lib/actions';
const machine = createMachine({
initial:"active",
states: {
active: {
entry: pure((context, event) => {
return [ raise('SOME_EVENT'), log("aaaaa") ]; //执行俩个动作 raise 和 log
}),
on: {
"SOME_EVENT":{ target:"inactive" }
}
},
inactive:{
type: "final"
}
}
});
invoke service(调用actor)
在一台状态机上表达整个应用程序的行为很快就会变得复杂和笨拙。 使用多个相互通信的状态机来表达复杂的逻辑是很自然的(并且受到鼓励!)。 这非常类似于Actor,其中每个状态机实例都被视为一个Actor,可以向其他Actor(例如 Promise 或其他状态机)发送和接收事件(消息)并对其做出响应 .
为了让状态机相互通信,父状态机调用子状态机并通过监听子状态机sendParent(...)发送的事件,或者等待子状态机到达其最终状态 ,这将导致进行onDone转换。
我们在状态上使用invoke属性来调用机器中的Actor(子Actor)。您可以在任何状态(包括根节点)上调用Actor。这些被调用的actors在状态中启动,并在退出状态时停止
invoke调用可以是:
1.promise,它将在resolve时触发onDone事件,或在reject时触发onError事件。
2.callback回调函数,它可以向父machine发送事件,也可以从父machine接收事件
3.Observable可观察对象,它可以向父machine发送事件,以及在完成时发送信号
4.machine状态机,它还可以发送/接收事件,并在到达最终状态时通知父machine
invoke会拦截报错 需onError处理
调用是在状态节点的配置中使用 invoke 属性定义的,其值是一个包含以下内容的对象:
1.src - 要调用的服务的来源,可以是:
状态机
一个返回 Promise 的函数
一个返回“回调处理程序”的函数
返回Oberverable可观察的函数
一个字符串,它指的是在这台状态机的 options.services 中定义的 4 个列出的选项中的任何一个
2.id - 被调用服务的唯一标识符
3.onDone - (可选)在以下情况下采用的转换:
子状态机达到其最终状态,或调用的 Promise 的 resolve,或被调用的 observable 完成
状态的 onDone(是复合节点到达最终状态) invoke.onDone(是完成的调用)
4.onError - (可选)当被调用的服务遇到执行错误时要进行的转换。
5.autoForward - (可选)true 如果发送到这台状态机的所有事件也应该发送(或 forwarded)到被调用的子节点(默认情况下为 false)
避免将 autoForward 设置为 true,因为盲目转发所有事件可能会导致意外行为和/或无限循环。 总是更喜欢显式发送事件,和/或使用 forward(...) 动作创建者直接将事件转发给被调用的孩子。 (目前仅适用于状态机)
6.data - (可选,仅在调用状态机时使用)将子状态机的 context 的属性映射到从父状态机的 context 返回相应值的函数的对象。
invoke调用promise/callback/oberverable等Actor:id作为Actor唯一标识
"invoke": [ { "src": "promise1","id": "promise1" }, {"src": "callback1","id": "callback1" } ]
向invoke的Actor发送事件:
send({ type: 'PING' }, { to: 'promise1' }) // to:'callback1'
0.基本属性介绍
import { createMachine, interpret, send, sendParent, done } from "xstate";
const minuteMachine = createMachine({
id: "timer",
initial: "active",
context: {
duration: 1000, // 默认 duration
},
states: {
active: {
after: {
6000: {
target: "finished",
actions: (ctx) => {
console.log(ctx);
},
},
},
},
finished: { type: "final" },
},
});
const parentMachine = createMachine({
id: "parent",
initial: "pending",
context: {
customDuration: 3000, // 默认 duration
},
states: {
pending: {
invoke: {//invoke调用新的子actor,进入状态时启动,退出后停止。不同machine下的invoke id可重复
// `src` 属性可以是: - a string in machine options.services
// - a machine
// - a function that returns...
src: (context, event) => {
// - a promise
// - a callback handler
// - an observable
},
id: 'timer1',//此为指定的id
//调用一个新的Promise子Actor,id为timer1,上下文与parentMachine的context一致,
// src: async(ctx,event)=>{ //async函数返回值必然是Promise(会把函数结果包裹在一个promise的resolve里面 Promise.resolve(结果) )
console.log(ctx); return 1; //onDone, 无return 为Promise.resolve(undefined);
throw new Error();//onError
},
//src: (ctx, event) => {
return new Promise((res, rej)=>{
console.log(ctx); res(1); //onDone
rej(0);//onError
})
}
// 将调用一个新的timerMachine 子Actor,id为timer1,初始上下文为data属性从父context派生的{duration: 3000 }
src: minuteMachine,
data: {// 从父上下文 派生子上下文
duration: (context, event) => context.customDuration,
},
//(可选)将状态机事件转发到被调用的服务(目前仅适用于状态机!)
autoForward: true,
//(可选)调用的promise/observable/machine完成时的转换
onDone: [ { target: /* ... */ } ],
// (可选)当被调用的服务发生错误时的转换
onError: [ { target: /* ... */ } ]
}
entry: send({ type: "PING" }, { to: "timer1" }),
},
timesUp: {
type: "final",
},
},
});
const service = interpret(parentMachine)
.onTransition((state) => console.log(state.value))
.start();
1.例如 promise案例:
由于每个 Promise 都可以建模为状态机,因此 XState 可以按原样调用 Promise。
resolve(), 这将采取onDone转换
reject() (或抛出错误),这将采用 onError 转换
如果在 Promise resolve 之前,退出调用Promise的状态,则 Promise 的结果将被丢弃
const fetchUser = (userId) => fetch(`url/to/user/${userId}`).then((response) => response.json()); //fetch: return new Promise();
const userMachine = createMachine({
id: 'user',
initial: 'idle',
context: {
userId: 42,
user: undefined,
error: undefined
},
states: {
idle: {
on: {
FETCH: { target: 'loading' }
}
},
loading: {
invoke: {
id: 'getUser',
src: (context, event) => fetchUser(context.userId), //fetchUser返回一个Promise
//或者直接async(context,event)=>{ ...... } //async是Promise语法糖,返回值会包在Promise.resolve(结果)中
onDone: {
target: 'success',
actions: assign({ user: (context, event) => event.data }) //invoke返回值
},
onError: {
target: 'failure',
actions: assign({ error: (context, event) => event.data })
}
}
}
});
2.调用 Callbacks
发送到父状态机的事件流可以通过回调处理程序建模,这是一个接受两个参数的函数:
callback - 向父状态机(state machine)发送的事件的调用
onReceive - 监听父状态机发送到被调用服务的事件
返回值(可选)是在退出当前状态时,对调用的服务执行清理(即取消订阅、防止内存泄漏等)的函数。回调不能使用 async/await 语法,因为它会自动将返回值包装在 Promise 中
例子1:invoke代替activities
counting: {
invoke: { //进入时执行src
id: 'incInterval',
src: (context, event) => (callback, onReceive) => { //callback模式固定写法,xstate自动传入callback和onReceive参数
// 这将每秒向父级发送 'INC' 事件
const id = setInterval(() => callback('INC'), 1000);
// 退出状态时执行清理
return () => clearInterval(id);
}
},
on: {
INC: { actions: assign({ counter: context => context.counter + 1 }) }
}
}
例子2:父子组件交互。父状态机向子 'ponger' 服务发送 'PING' 事件。子服务可以使用 onReceive(listener) 监听该事件,并将一个 'PONG' 事件发送回父级作为响应
const pingPongMachine = createMachine({
id: 'pinger',
initial: 'active',
states: {
active: {
invoke: {
id: 'ponger',
src: (context, event) => (callback, onReceive) => {
// 每当父级发送“PING”时, // 发送到父'PONG'事件
onReceive((e) => {
if (e.type === 'PING') {
callback('PONG'); //向父状态机(state machine)发送的事件
}
});
}
},
entry: send({ type: 'PING' }, { to: 'ponger' }),
on: {
PONG: { target: 'done' }
}
},
done: {
type: 'final'
}
}
});
interpret(pingPongMachine)
.onDone(() => done()) // 监听根状态达到final状态
.start();
3.调用Observables
Observables (opens new window)是随时间发出的值流。 将它们视为一个数组/集合,其值是异步发出的,而不是一次发出。 JavaScript 中有许多 observable 的实现; 最受欢迎的是 RxJS (opens new window)。
可以调用 Observables,它应该向父状态机发送事件(字符串或对象),但不接收事件(单向)。 一个 observable 调用是一个函数,它以 context 和 event 作为参数并返回一个可观察的事件流。 当退出调用它的状态时,observable 被取消订阅。
import { createMachine, interpret } from 'xstate';
import { interval } from 'rxjs';
import { map, take } from 'rxjs/operators';
const intervalMachine = createMachine({
id: 'interval',
initial: 'counting',
context: { myInterval: 1000 },
states: {
counting: {
invoke: {
src: (context, event) =>
interval(context.myInterval).pipe( //Observable流
map((value) => ({ type: 'COUNT', value })), //触发父COUNT事件
take(5)
),
onDone: 'finished'
},
on: {
COUNT: { actions: 'notifyCount' },
CANCEL: { target: 'finished' }
}
},
finished: {
type: 'final'
}
}
});
4.调用 Machines
状态机分层通信,被调用的状态机可以通信:
通过send(EVENT, { to: 'someChildId' }) 动作实现父到子
通过 sendParent(EVENT) 操作实现子级到父级。
service.send(eventType, {to : 'ponger'}) //目标actor只能指定本machine下的actor(包括子Actor)。若是其他machine的actor报错如下:
Unable to send event to child 'ponger' from service 'pinger'
子状态机达到最终状态时触发onDone监听,如果退出调用状态机的状态,则状态机停止。
// 调用子状态机
const minuteMachine = createMachine({
id: 'timer',
initial: 'active',
context: {
duration: 1000 // 默认 duration
},
states: {
active: {
after: {
60000: { target: 'finished' }
}
},
finished: { type: 'final' }
}
});
const parentMachine = createMachine({
id: 'parent',
initial: 'pending',
context: {
customDuration: 3000
},
states: {
pending: {
invoke: {
src: minuteMachine,
// 当 minuteMachine 达到其顶级最终状态时,将进行 onDone 转换。
onDone: 'timesUp',
data: { //从父上下文,派生子上下文
duration: (context, event) => context.customDuration
}
}
},
timesUp: {
type: 'final'
}
}
});
Actor概念 & spawn创建Actor
当您运行statechart时,它就变成了一个Actor(每个machine都是一个Actor):一个正在运行的进程,它可以接收消息、发送消息,并根据它接收到的消息更改其行为。通常,您需要您的actor来运行其他actor;生成新的状态图,等待承诺,或订阅可观察对象。
Actor模型是一种基于消息的计算的数学模型,它简化了多个“实体”(或“Actor”)相互通信的方式。 Actor通过相互发送消息(事件)来进行通信。 Actor的本地状态是私有的,除非它希望通过将其作为事件发送来与另一个Actor共享。
Activities类似Actor,已弃用,并将在XState版本5中删除。推荐的方法是调用Actor(invoke)
当一个Actor收到一个事件时,能发生三件事:
1.有限数量的消息可以 send给其他Actor
2.可以创建(或 spawned)有限数量的新Actor
3.Actor的本地状态可能会改变(由其 behavior 决定)
service.send(eventType, {to : 'ponger'}) //目标actor只能指定本machine下的actor(包括子Actor)。若是其他machine的actor报错如下:
Unable to send event to child 'ponger' from service 'pinger'
状态机和状态图与 Actor 模型配合得很好,因为它们是基于事件的行为和逻辑模型。 当状态机因事件而转换时,下一个状态包含:
1.下一个 value 和 context( Actor 的本地状态)
2.要执行的下一个 actions (可能是新生成的 Actor 或发送给其他 Actor 的消息)
Actor 可以是 创建的 或 调用的。 spawn创建的Actor(子Actor):
他们可以在任何时候被创建(通过 assign(...) 操作中的 spawn(...))
他们可以随时停止(通过stop(...)动作)
Actor 在XState中实现具有以下接口:
一个 id 属性,它在本地系统(本machine)中唯一标识Actor
一个 .send(...) 方法,用于向这个 Actor 发送事件 //to目标actor只能指定本machine下的actor(包括子Actor)。
一个.getSnapshot()方法,同步返回Actor的最后 触发值。
可选的方法:
一个 .stop() 方法,它停止 Actor 并执行任何必要的清理
一个.subscribe(...) 方法。
所有现有的调用服务模式都适合这个接口:promise/callback/observables/machine
Actor的 触发值 是订阅者在 Actor 的.subscribe(...) 方法中收到的值。
对于 service,发出当前状态。
对于 promise,发出 resolve 的值(如果未实现,则为“未定义”)。
对于 observables,发出最新发出的值。
对于 callback,不会发出任何内容。
spawn(...) 函数通过提供 1 或 2 个参数来创建 Actor 引用:
entity - 代表Actor的值或状态机。 entity类型:Machine/Promise/Callback/Observable
name(可选)- 唯一标识 Actor 的字符串。这对于所有生成的Actor和调用的服务应该是唯一的。
spawn创建Actor的例子:
const loginMachine = createMachine({
// ...
entry: assign({
formRef: () => spawn(formMachine, 'form')
}),
exit:[
stop((context) => context.formRef), // 通过引用停止一个actor
stop('form') // 通过 ID 停止 actor
],
states: {
idle: {
on: {
LOGIN: {
actions: send({ type: 'SUBMIT' }, { to: 'form' })
// {to: (context)=>{context.formRef}}
}
}
}
}
});
model
createModel(…)函数已弃用,将在XState版本5中被移除。建议使用Typegen。
在XState中,您可以使用createModel(…)对machine的上下文和事件进行外部建模。这提供了一种方便的方法来强类型上下文和事件,以及将来用于事件创建、赋值和其他实现细节的帮助程序。
使用createModel(…)是完全可选的,它的目的是改善开发人员的体验。使用它的主要原因是:
以强类型的方式分离和组织上下文和事件
防止使用assign(…)
指定事件创建者,以便更容易和更安全地创建事件
潜在地与其他机器共享模型
未来开发人员体验的改进,例如指定操作、保护等。
const userModel = createModel({
name: 'Someone', //context
age: 0
}, {
events: { // Event creators
updateName: (value) => ({ value }),
updateAge: (value) => ({ value }),
anotherEvent: () => ({}) // no payload
}
});
const machine = userModel.createMachine({
context: userModel.initialContext,
// ...
entry: userModel.assign({ name: '' }) //修改userModel的context
});
context上下文
context是全局共享的:machine中所有state都可以通过state.context获取上下文
const glassMachine = createMachine(
{
id: 'glass',
// the initial context (extended state) of the statechart
context: {
amount: 0
},
initial: 'empty',
}
const nextState = glassMachine.transition(glassMachine.initialState, {type: 'FILL'});
console.log(nextState.context); // => { amount: 1 }
在XState中为上下文分配新值是通过assign动作完成的,这是改变机器上下文的唯一方法。永远不要从外部改变机器的上下文。,每个上下文更改都应该显式地由事件触发。
actions: {
assignToContext: assign((context) => { //更改context的值
return {
count: context.count + 1,
、 message: 'Count changed',
};
}),
}
工作流 & 状态机区别
工作流引擎和状态机的主要区别在于焦点。
在工作流引擎中,在完成之前的操作时,就会切换到下一个步骤,而状态机需要一个外部事件,这将导致分支到下一个活动。换句话说,状态机是事件驱动的,关注状态流转,而工作流引擎不是,关注单个任务(比如 描述一个 Task 他完成什么样的动作,以及他依赖什么样的动作)。