前端状态机系列:SCXML与XState对应关系
1. 前置說明
這次再說明下自己對狀態(tài)圖的看法。狀態(tài)圖雖然有非常多的優(yōu)勢(參考上篇文章),如果你想使用,關(guān)于是否對整個舊項目進行全量狀態(tài)圖化,這里給一個適應(yīng)范圍是:項目中復(fù)雜的部分進行狀態(tài)圖建模是非常合適的。如果你有精力是可以嘗試對整個項目進行狀態(tài)圖化的。
1.1 狀態(tài)圖
再回顧一下什么是狀態(tài)圖。
狀態(tài)圖的前身是狀態(tài)機(FSM),FSM 使用過程中會暴露一些問題,如:
- 狀態(tài)爆炸
- 層次表達能力弱
項目復(fù)雜起來,到后期 FSM 會很難維護。
針對這些問題,計算機科學(xué)家 David Harel 在 1984 年對 FSM 進行了擴展,發(fā)明了 狀態(tài)圖(SC)來解決 FSM 中的問題。(論文地址)
SC 不僅僅是更好的可視化了 FSM,而且它是可執(zhí)行的。現(xiàn)在的大多數(shù)狀態(tài)機工具庫,更確切的說應(yīng)該是狀態(tài)圖工具庫。
SC 定義為一個分層有向圖(S,T,R,In,Out),比 FSM 多了一個 R(Orthogonal 正交)的概念。
SC 設(shè)計了一套非常復(fù)雜且非常精確的符號系統(tǒng),增強了結(jié)構(gòu)層次的表達能力和有向圖的連通表達能力。目前也是 UML 的首選控制模型。
1.2 SCXML
SCXML 全稱 State Chart XML,用于控制抽象的狀態(tài)機表示法。
SCXML 是基于上面說的 David Harel 狀態(tài)圖 和 CCXML(Call Control eXtensible Markup Language) 進行擴展的一套規(guī)范。
從 2005 年到 2015 年經(jīng)歷 10 年定制的規(guī)范,成為 W3C 推薦規(guī)范。目前大部分編程語言的狀態(tài)機工具都是基于此規(guī)范實施的。
1.3 XState
XState 是一個前端的狀態(tài)圖工具庫,由微軟工程師 David Khourshid 開發(fā)。目前是前端狀態(tài)機里面 Star 最多的,本人體驗下來感覺也很不錯(本人很高興在此倉庫貢獻了 14.7k 行 )。下圖是 XState Github Star 記錄:
2. 組織說明
XState 的文檔寫的并不是很好懂,很多概念跳來跳去(當然 大多數(shù)國外的文檔都有這種問題,作者肯定很想表達清楚,但并不容易做),如果讀者對狀態(tài)機沒有概念,突如其來的一堆新的概念會讓你措手不及,學(xué)習(xí)曲線劇增,使用上也不知該如何下手。
如果想要對這些概念有更好的認識和組織,那用 SCXML 和 XState 去對照著看,或許是比較合適的。
2.1 SCXML 的組織
主要有以下部分:
- 核心
- <scxml>
- <state>
- <transition>
- <initial>
- <parallel>
- <final>
- <history>
- <onentry>
- <onexit>
- 可執(zhí)行內(nèi)容
- <raise>
- <foreach>
- <log>
- <if>
- <elseif>
- <else>
- 數(shù)據(jù)模型和數(shù)據(jù)操作
- <datamodel>
- <data>
- <content>
- <param>
- <donedata>
- <script>
- <assign>
- 外部通訊
- <send>
- <cancel>
- <invoke>
- <finalize>
2.2 XState 的組織
主要有以下部分:
- Machine
- State
- State Node
- Event
- Transition
- Parallel State
- Final State
- History State
- Effects
- Invoke
- Actions
- send
- raise
- respond
- forwardTo
- escalate
- log
- choose
- pure
- assign
- Activities
- Context
- Guard
- Delay
- Interpret
- Identify
- Actor
- Model
3. 對應(yīng)關(guān)系
下面以 SCXML 為主線去做對應(yīng)描述。
3.1 核心元素
按照 SCXML 的分類,先從核心部分的元素進行對應(yīng)說明。
3.1.1 <scxml>
<scxml>,最外層的狀態(tài)機包裹元素,攜帶版本信息,狀態(tài)機是由它的 children 組成的。
屬性字段描述如下:
| initial | false | none | IDREFS | none | 合法的狀態(tài)規(guī)范 | 狀態(tài)機的初始狀態(tài)的 id。如果未指定,則默認初始狀態(tài)是文檔順序中的第一個子狀態(tài) |
| name | false | none | NMTOKEN | none | 任何有效的NMTOKEN | 此狀態(tài)機的名稱。它純粹是為了提供信息 |
| xmlns | true | none | URI | none | http://www.w3.org/2005/07/scxml | |
| version | true | none | decimal | none | 必須 “1.0” | |
| datamodel | false | none | NMTOKEN | platform-specific | “null”, “ecmascript”, “xpath” 或者其他平臺定義的值 | 本文檔所需的數(shù)據(jù)模型。 “null”表示 Null 數(shù)據(jù)模型,“ecmascript”表示 ECMAScript 數(shù)據(jù)模型,“xpath”表示 XPath 數(shù)據(jù)模型 |
| binding | false | none | enum | “early” | “early”, “l(fā)ate” | 要使用的數(shù)據(jù)綁定 |
children 可以包含:
- <state>
- <parallel>
- <final>
- <datamodel>
- <script>
對應(yīng) XState 是 Machine,Machine 的部分屬性描述如下(詳情):
{"id": "","initial": "","context": {},"states": {} }3.1.2 <state>
<state>,用來描述狀態(tài)機中的狀態(tài)。
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml"><state id="狀態(tài)A"/> </scxml>屬性字段描述如下:
| id | false | none | ID | none | 狀態(tài) ID | |
| initial | false | 不得與 元素一起指定。絕不能以原子狀態(tài)出現(xiàn)。 | IDREFS | none | 此狀態(tài)的默認初始狀態(tài) |
children 可以包含:
- <onentry>
- <onexit>
- <transition>
- <initial>
- <state>
- <parallel>
- <final>
- <history>
- <datamodel>
- <invoke>
對應(yīng)XState 的 State Node。不過 State Node 是一個 SCXML 多個元素組成的一個屬性。由 <state>、<initial>、<parallel>、<final>、<history>組成。
State Node 的部分屬性描述如下(詳情):
{"id": "","states": {},"invoke": {},"on": {},"onEntry": {},"onExit": {},"onDone": {},"always": {},"after": {},"tags": [],"type": "" }示例:
Machine({id: "狀態(tài)機",states: {狀態(tài)A: {id: "狀態(tài)A",},}, })3.1.3 <transition>
狀態(tài)之間進行轉(zhuǎn)換。由事件觸發(fā),通過條件判斷后進行轉(zhuǎn)換。
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml"><state id="打開"><transition cond="_event.data==1" event="點擊" target="關(guān)閉" /></state><state id="關(guān)閉" /> </scxml>屬性字段描述如下:
| event | false | EventsTypes.datatype | none | 以空格分隔的事件描述符列表 | 觸發(fā)此轉(zhuǎn)換的事件指示符列表 | |
| cond | false | Boolean expression | ‘true’ | 布爾表達式 | 轉(zhuǎn)換條件 | |
| target | false | IDREFS | none | 要跳轉(zhuǎn)到的狀態(tài) | 要轉(zhuǎn)換到的狀態(tài)或并行區(qū)域的標識符 | |
| type | false | enum | “external” | “internal” “external” | 確定目標狀態(tài)是來自于內(nèi)部轉(zhuǎn)換還是外部轉(zhuǎn)換 |
children 可以包含 可執(zhí)行內(nèi)容。
對應(yīng)XState 的 Event、Transition、Guard。部分屬性描述如下(詳情):
{"on": {"": {},"*": {},"自定義事件": {"target": "目標狀態(tài)","cond": "條件判斷","actions": "可執(zhí)行內(nèi)容","in": "只能從這個狀態(tài)過來","internal": "內(nèi)部轉(zhuǎn)換","meta": {},"description": ""}} }示例:
Machine({id: "狀態(tài)機",states: {打開: {on: {點擊: {target: "關(guān)閉",cond: (ctx, event) => event.data == 1,},},},關(guān)閉: {},}, });3.1.4 <initial>
<initial>,表示復(fù)雜 元素(即包含子 或 元素的元素)的默認初始狀態(tài)。并不是一個狀態(tài),只是一個指向狀態(tài)的作用。
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml"><state id="打開"><initial><transition target="寫入" /></initial><state id="寫入" /><state id="讀取" /></state> </scxml>必須和 <transition> 一起使用,進行狀態(tài)指定。
children 包含 <transition>。
XState 可以直接在 State Node 的 initail 進行指定實現(xiàn)。
示例:
Machine({id: "狀態(tài)機",states: {打開: {initial: "讀取",states: {讀取: {},寫入: {},},},}, });3.1.5 <parallel>
該元素表示一個狀態(tài),其子項并行執(zhí)行。當父元素處于活動狀態(tài)時,子項同時處于活動狀態(tài)。
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml"><parallel id="網(wǎng)盤"><state id="寫入" /><state id="讀取" /></parallel> </scxml>屬性字段描述如下:
| id | false | none | ID | none | XML Schema 中定義的有效 id | 狀態(tài) ID |
children 可以包含:
- <onentry>
- <onexit>
- <transition>
- <state>
- <parallel>
- <history>
- <datamodel>
- <invoke>
XState 可以直接在 State Node 的 type: parallel 進行指定實現(xiàn)。
示例:
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="網(wǎng)盤"><parallel id="網(wǎng)盤"><state id="上傳"><initial><transition target="空閑" /></initial><state id="空閑"><transition target="上傳中" event="開始" /></state><state id="上傳中"><transition target="成功" event="完在" /></state><state id="成功"></state></state><state id="下載"><initial><transition target="下載.空閑" /></initial><state id="下載.空閑"><transition target="下載.下載中" event="開始" /></state><state id="下載.下載中"><transition target="下載.成功" event="完在" /></state><state id="下載.成功"></state></state></parallel> </scxml> Machine({id: "狀態(tài)機",initial: "網(wǎng)盤",states: {網(wǎng)盤: {type: "parallel",states: {下載: {initial: "空閑",states: {空閑: {on: {開始: "下載中",},},下載中: {on: {完成: "成功",},},成功: {},},},上傳: {initial: "空閑",states: {空閑: {on: {開始: "上傳中",},},上傳中: {on: {完成: "成功",},},成功: {},},},},},}, });3.1.6 <final>
<final> 表示 <scxml> 或復(fù)合 <state> 元素的最終狀態(tài)。
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml"><state id="下載中"><transition event="完成" target="成功" /></state><final id="成功" /> </scxml>屬性字段描述如下:
| id | false | none | ID | none | XML Schema 中定義的有效 id | 狀態(tài) ID |
children 可以包含:
- <onentry>
- <onexit>
- <donedata>
XState 可以直接在 State Node 的 type: final 進行指定實現(xiàn)。
示例:
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml"><initial><transition target="工作" /></initial><state id="工作"><initial><transition target="正在完成任務(wù)" /></initial><!-- 子狀態(tài)為 final 時,父狀態(tài)觸發(fā) don.state 事件 --><transition event="done.state.工作" target="工作完成" /><state id="正在完成任務(wù)"><transition event="完成" target="任務(wù)完成" /></state><final id="任務(wù)完成"></final></state><final id="工作完成" /> </scxml> Machine({id: "狀態(tài)機",initial: "工作",states: {工作: {initial: "正在完成任務(wù)",states: {正在完成任務(wù): {on: {完成: "任務(wù)完成",},},任務(wù)完成: {type: "final",},},onDone: "工作完成",},工作完成: {},}, });3.1.7 <history>
<history> 偽狀態(tài)允許狀態(tài)機記住它的狀態(tài)配置。以 <history> 狀態(tài)為目標的 <transition> 會將狀態(tài)機返回到此記錄的配置。
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml"><history id="歷史狀態(tài)" type="shallow"><transition target="狀態(tài)A" /></history><state id="狀態(tài)A"></state> </scxml>屬性字段描述如下:
| id | false | none | ID | none | XML Schema 中定義的有效 id | 狀態(tài) ID |
| type | false | none | enum | “shallow” | “deep” 或 “shallow” | 確定是記錄當前狀態(tài)的活動原子子狀態(tài)還是僅記錄其直接活動子狀態(tài)。 |
children 可以包含 <transition>。
<transition> ‘target’ 指定默認歷史配置的轉(zhuǎn)換。 僅發(fā)生一次。 在符合標準的 SCXML 文檔中,此轉(zhuǎn)換不得包含“cond”或“事件”屬性,并且必須指定一個非空“target”。此轉(zhuǎn)換可能包含可執(zhí)行內(nèi)容。 如果 ‘type’ 是“shallow”,那么這個 <transition> 的 ‘target’ 必須只包含父狀態(tài)的直接子級。 否則,它必須只包含父級的后代。
XState 可以直接在 State Node 的 type: history 進行指定實現(xiàn)。多了一些額外屬性:
{"type": "history","history": "shallow","target": "默認指定到父狀態(tài)" }示例:
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="新建"><state id="新建"><initial><transition target="編寫中"></transition></initial><transition target="中斷" event="暫停"></transition><state id="編寫中"><transition target="預(yù)覽中" event="下一步"></transition></state><state id="預(yù)覽中"><transition target="提交中" event="下一步"></transition></state><state id="提交中"></state><history id="歷史狀態(tài)" type="shallow"></history></state><state id="中斷"><transition target="歷史狀態(tài)" event="恢復(fù)"></transition></state></scxml> Machine({id: "狀態(tài)機",initial: "新建",states: {新建: {initial: "編寫中",on: {暫停: "中斷",},states: {編寫中: {on: {下一步: "預(yù)覽中",},},預(yù)覽中: {on: {下一步: "提交中",},},提交中: {},歷史狀態(tài): {type: "history",},},},中斷: {on: {恢復(fù): "新建.歷史狀態(tài)",},},}, });3.1.8 <onentry>
<onentry>,一個包裝元素,包含進入狀態(tài)時要執(zhí)行的可執(zhí)行內(nèi)容。
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="狀態(tài)A"><state id="狀態(tài)A"><onentry><log expr="'歡迎進入狀態(tài)A'" /></onentry></state> </scxml>同 <transition>,children 只能包含 可執(zhí)行內(nèi)容。
XState 可以直接在 State Node 的 onEntry 進行定義。
示例:
Machine({id: "狀態(tài)機",initial: "狀態(tài)A",states: {狀態(tài)A: {onEntry: actions.log("歡迎進入狀態(tài)A"),},}, });3.1.9 <onexit>
<onexit>,一個包裝元素,包含退出狀態(tài)時要執(zhí)行的可執(zhí)行內(nèi)容。
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="狀態(tài)A"><state id="狀態(tài)A"><onexit><log expr="'歡迎下次再來狀態(tài)A'" /></onexit></state> </scxml>同 ,children 只能包含 可執(zhí)行內(nèi)容。
XState 可以直接在 State Node 的 onExit 進行定義。
示例:
Machine({id: "狀態(tài)機",initial: "狀態(tài)A",states: {狀態(tài)A: {onExit: actions.log("歡迎下次再來狀態(tài)A"),},}, });3.2 可執(zhí)行內(nèi)容
可執(zhí)行內(nèi)容,只能在 <onentry>、<onexit> 和 <transition> 中使用。它提供了允許 SCXML 會話修改其數(shù)據(jù)模型并與外部實體交互的鉤子。
不僅包括了 <raise>、<foreach>、<log>、<if>、<elseif>、<else>,還包含了其他分組下的 <script> 、<assign>、<send> 、<cancel>。當然 下面我們還是按照規(guī)范文檔中的分類進行對應(yīng)說明。
在 XState 中,所有的在 SCXML 中的“可執(zhí)行內(nèi)容”統(tǒng)稱為 action。所以對應(yīng)的這些“可執(zhí)行內(nèi)容”都在 XState 的 actions 包中。
3.2.1 <raise>
<raise> 元素在當前 SCXML 會話中引發(fā)一個事件。可以觸發(fā) <transition> 中的 event。
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="狀態(tài)A"><state id="狀態(tài)A"><onentry><raise event="跳轉(zhuǎn)" /></onentry></state> </scxml>屬性字段描述如下:
| event | true | NMTOKEN | none | 指定事件的名稱。這將與轉(zhuǎn)換的“event”屬性相匹配。 |
對應(yīng) XState 的 actions.raise 函數(shù)。示例:
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="狀態(tài)A"><state id="狀態(tài)A"><transition target="狀態(tài)B" event="跳轉(zhuǎn)"></transition><onentry><raise event="跳轉(zhuǎn)" /></onentry></state><state id="狀態(tài)B"></state> </scxml> Machine({id: "狀態(tài)機",initial: "狀態(tài)A",states: {狀態(tài)A: {onEntry: actions.raise("跳轉(zhuǎn)"),on: {跳轉(zhuǎn): "狀態(tài)B",},},狀態(tài)B: {},}, });3.2.2 <foreach>
<foreach> 元素允許 SCXML 應(yīng)用程序遍歷 <datamodel> 中的集合,并為集合中的每個項目執(zhí)行其中包含的 可執(zhí)行內(nèi)容。
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="狀態(tài)A"><datamodel><data expr="[ 10, 20, 30 ]" id="dataArr" /></datamodel><state id="狀態(tài)A"><onentry><foreach array="dataArr" index="varIndex" item="varItem"><log expr="varIndex" /><log expr="varItem" /></foreach></onentry></state> </scxml>屬性字段描述如下:
| array | true | Value expression | none | 計算結(jié)果為可迭代集合的值表達式 <foreach> | 元素將遍歷此集合的淺表副本 | |
| item | true | xsd:string | none | 在指定數(shù) <datamodel> 中有效的任何變量名 | 在循環(huán)的每次迭代中存儲集合的不同項的變量 | |
| index | false | xsd:string | none | 在指定數(shù) <datamodel> 中有效的任何變量名 | 在 foreach 循環(huán)的每次迭代中存儲當前迭代索引的變量 |
children 由一個或多個 可執(zhí)行內(nèi)容組成。
可以對應(yīng) XState 的 actions.pure 函數(shù),它也可以返回一個或者一組 action,或者什么也不返回。當然這個函數(shù)更靈活。示例:
Machine({id: "狀態(tài)機",initial: "狀態(tài)A",context: {dataArr: [10, 20, 30],},states: {狀態(tài)A: {onEntry: actions.pure((context, event) => {const _actions = [];context.dataArr.map((varItem, varIndex) => {_actions.push(actions.log(varIndex.toString()));_actions.push(actions.log(varItem.toString()));});return _actions;}),},}, });3.2.3 <log>
<log> 允許應(yīng)用程序生成日志記錄或調(diào)試消息。
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="狀態(tài)A"><state id="狀態(tài)A"><onentry><log expr="'歡迎進入狀態(tài)A'" /></onentry></state> </scxml>屬性字段描述如下:
| label | false | string | 空字符串 | 具有依賴于實現(xiàn)的解釋的字符串。它旨在提供有關(guān)“expr”指定的日志字符串的元數(shù)據(jù)。 | ||
| expr | false | 值表達式 | none | 返回要記錄的值的表達式 |
對應(yīng) XState 的 actions.log。示例:
Machine({id: "狀態(tài)機",initial: "狀態(tài)A",states: {狀態(tài)A: {onEntry: actions.log("歡迎進入狀態(tài)A"),},}, });3.2.4 <if>、<elseif>、<else>
<if> 是條件執(zhí)行元素的容器。
<elseif> 是一個空元素,它對 <if> 的內(nèi)容進行分區(qū),并提供一個判斷是否執(zhí)行分區(qū)的條件。
<else> 是一個空元素,用于劃分 <if> 的內(nèi)容。它等價于一個帶有“cond”的 <elseif>,它總是計算為真。
屬性字段描述如下:
| cond | true | 條件表達式 | none | 有效的條件表達式 | 一個布爾表達式 |
XState 中有很多方法可以實現(xiàn)類似的能力,如果非要對標的話就是 actions.choose 函數(shù)了。示例:
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="狀態(tài)A"><datamodel><data expr="2" id="value" /></datamodel><state id="狀態(tài)A"><onentry><if cond="value == 1"><log expr="'value === 1'" /><elseif cond="value == 2" /><log expr="'value === 2'" /><else /><log expr="'value != 1 && value != 2'" /></if></onentry></state> </scxml> Machine({id: "狀態(tài)機",initial: "狀態(tài)A",context: {value: 2,},states: {狀態(tài)A: {onEntry: actions.choose([{cond: (context, event) => context.value === 1,actions: [actions.log("value === 1")],},{cond: "equal2",actions: [actions.log("value === 2")],},{actions: [actions.log("value != 1 && value != 2")],},]),},},},{guards: {equal2: (context) => context.value === 2,},} );3.3 數(shù)據(jù)模型和數(shù)據(jù)操作
這部分是狀態(tài)之外的數(shù)據(jù)部分的定義和操作。
數(shù)據(jù)模型通過 <datamodel> 元素定義,該元素包含零個或多個 <data> 元素,每個元素定義一個數(shù)據(jù)元素并為其分配一個初始值。 這些值可以在線指定或從外部源加載。 然后可以通過 <assign> 元素更新它們。 <donedata>、<content> 和 <param> 元素可用于將數(shù)據(jù)合并到與外部實體的通信中。 最后,<script> 元素允許結(jié)合腳本。
3.3.1 <datamodel>
<datamodel> 是一個包裝器元素,它封裝了任意數(shù)量的 <data> 元素,每個元素都定義了一個數(shù)據(jù)對象。
children 只能包含 <data>。
對應(yīng) XState 的頂層 context。
3.3.2 <data>
<data> 元素用于聲明和填充數(shù)據(jù)模型的部分。
<datamodel><data expr="true" id="VarBool" /><data expr="1" id="VarInt" /><data expr="'這是字符串'" id="VarString" /> </datamodel>屬性字段描述如下:
| id | true | ID | none | 數(shù)據(jù)項的名稱 | ||
| src | false | URI | none | 給出應(yīng)從中獲取數(shù)據(jù)對象的位置 | ||
| expr | false | Expression | none | 有效的條件表達式 | 執(zhí)行以提供數(shù)據(jù)項的值 |
在符合標準的 SCXML 文檔中,<data> 元素可以具有“src”或“expr”屬性,但不能 同時具有。此外,如果任一屬性存在,<data> 元素 絕不能有 children。因此,‘src’、‘expr’ 和 children 在 <data> 元素中是互斥的。
在 XState 中直接作為 context 字段的值存在。
示例:
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="狀態(tài)A"><datamodel><data expr="true" id="VarBool" /><data expr="1" id="VarInt" /><data expr="'這是字符串'" id="VarString" /></datamodel><state id="狀態(tài)A"><onentry><log expr="VarBool" /><log expr="VarInt" /><log expr="VarString" /></onentry></state> </scxml> Machine({id: "狀態(tài)機",initial: "狀態(tài)A",context: {varBool: true,varInt: 1,varString: "這是字符串",},states: {狀態(tài)A: {onEntry: [actions.log((context) => context.varBool),actions.log((context) => context.varInt),actions.log((context) => context.varString),],},}, });3.3.3 <assign>
<assign> 元素用于修改數(shù)據(jù)模型。
<assign location="Var1" expr="5"/>屬性字段描述如下:
| location | true | 路徑表達式 | none | 有效的路徑表達式 | 數(shù)據(jù)模型中要插入新值的位置 | |
| expr | false | 此屬性不得出現(xiàn)在具有子元素的 <assign> 元素中 | 值表達 | none | 有效的值表達式 | 返回要分配的值的表達式 |
在 XState 中用 actions.assign 函數(shù)表示。
示例:
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="狀態(tài)A"><datamodel><data expr="1" id="VarInt" /><data expr="'這是字符串'" id="VarString" /></datamodel><state id="狀態(tài)A"><onentry><assign expr="5" location="VarInt" /><assign expr="'新的字符串'" location="VarString" /></onentry></state> </scxml> Machine({id: "狀態(tài)機",initial: "狀態(tài)A",context: {varInt: 1,varString: "這是字符串",},states: {狀態(tài)A: {onEntry: actions.assign({varInt: 5,varString: "新的字符串",}),},}, });3.3.4 <script>
<script> 元素將腳本功能添加到狀態(tài)機。
<script>console.log('Hello, world!')</script>屬性字段描述如下:
| src | false | 如果元素有子元素,則可能不會發(fā)生 | none | 有效的 URI | 給出應(yīng)該下載腳本的位置 |
<script> 元素的 children 內(nèi)容表示要執(zhí)行的腳本代碼。
XState 在很多地方可以表達類似的能力,如 actions 屬性是支持直接賦值函數(shù)的,在 actions.log、actions.pure、actions.assign 等函數(shù)都可以實現(xiàn)類似能力。
3.3.5 <donedata>
一個包裝元素,保存進入 <final> 狀態(tài)時要返回的數(shù)據(jù)。
<final id="最終狀態(tài)"><donedata><param expr="'value1'" name="key1" /><param expr="'value2'" name="key2" /></donedata> </final>children 可以包含:
- <content>:可以出現(xiàn) 0 次或 1次。
- <param>:可以出現(xiàn) 0 次或多次。
一個符合標準的 SCXML 文檔必須指定單個 <content> 元素或一個或多個 <param> 元素作為 <donedata> 的子元素,但不能同時指定兩者。
如果 SCXML 處理器在進入最終狀態(tài)時生成“done”事件,它必須執(zhí)行 <donedata> 元素 <param> 或 <content> 子元素并將結(jié)果數(shù)據(jù)放在 _event.data 字段中。
對應(yīng)到 XState State Node 的 data 屬性字段。
示例:
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="狀態(tài)A"><state id="狀態(tài)A" initial="狀態(tài)A1"><transition target="狀態(tài)B" event="done.state.狀態(tài)A"><log expr="_event.data"></log></transition><final id="狀態(tài)A1"><donedata><param expr="1" name="finalCustomeData1" /><param expr="2" name="finalCustomeData2" /></donedata></final></state><state id="狀態(tài)B"></state> </scxml> Machine({id: "狀態(tài)機",initial: "狀態(tài)A",context: {},states: {狀態(tài)A: {initial: "狀態(tài)A1",onDone: {actions: actions.log((context, event) => {return event.data;}),target: "狀態(tài)B",},states: {狀態(tài)A1: {type: "final",data: {finalCustomeData1: 1,finalCustomeData2: 2,},},},},狀態(tài)B: {},}, });3.3.6 <param>
<param> 標簽提供了一種識別鍵和動態(tài)計算值的通用方法,該值可以傳遞給外部服務(wù)或包含在事件中。
<final id="最終狀態(tài)"><donedata><param expr="'value1'" name="key1" /><param expr="'value2'" name="key2" /></donedata> </final>屬性字段描述如下:
| name | true | NMTOKEN | none | 字符串 | ||
| expr | false | 值表達式 | none | 有效值表達式 | ||
| location | false | 位置表達式 | none | 有效位置表達式 |
類似于一個 key 一個 value 的方式定義事件數(shù)據(jù)。
XState 比較靈活,直接在事件返回處填寫 Object 即可。
3.3.7 <content>
包含要傳遞給外部服務(wù)的數(shù)據(jù)的容器元素。
<final id="最終狀態(tài)"><donedata><content>{key1: 'value1', key2: 'value2'}</content></donedata> </final>屬性字段描述如下:
| expr | false | 不得與子內(nèi)容一起出現(xiàn) | 值表達式 | none | 有效值表達式 |
如果“expr”屬性不存在,處理器必須使用 <content> 的子元素作為輸出。
類似于一個 Object 的方式定義事件數(shù)據(jù)。功能和 <param> 相似。可以在 <donedata>、<send>、<invoke> 中使用。
XState 比較靈活,直接在事件返回處填寫 Object 即可。
3.4 外部通訊
外部通信功能允許 SCXML 會話從外部實體發(fā)送和接收事件,并調(diào)用外部服務(wù)。
3.4.1 <send>
<send> 用于將事件和數(shù)據(jù)發(fā)送到外部系統(tǒng),包括外部 SCXML 解釋器,或在當前 SCXML 會話中引發(fā)事件。提供“即發(fā)即棄”的能力。
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="狀態(tài)A"><state id="狀態(tài)A"><onentry><send event="跳轉(zhuǎn)" /></onentry></state> </scxml>屬性字段描述如下:
| event | false | 不得與“eventexpr”一起出現(xiàn) | EventType.datatype | none | 一個字符串,指示正在生成的消息的名稱 | |
| eventexpr | false | 不得與“event”一起出現(xiàn) | 值表達式 | none | “event”的動態(tài)替代方案。如果此屬性存在,SCXML 處理器必須在執(zhí)行父 <send> 元素時對其進行執(zhí)行,并將結(jié)果視為已作為“event”的值輸入 | |
| target | false | 不能與“targetexpr”一起出現(xiàn) | URI | none | 有效的目標 URI | 平臺應(yīng)將事件發(fā)送到的消息目標的唯一標識符 |
| targetexpr | false | 不能與“target”一起出現(xiàn) | 值表達式 | none | 有效目標 URI 的表達式 | “target”的動態(tài)替代方案。如果存在此屬性,則 SCXML 處理器必須在執(zhí)行父 <send> 元素時對其進行執(zhí)行,并將結(jié)果視為已作為“target”的值輸入 |
| type | false | 不能與“typeexpr”一起出現(xiàn) | URI | none | 標識消息傳輸機制的 URI | |
| typeexpr | false | 不能與“type”一起出現(xiàn) | 值表達式 | none | “type”的動態(tài)替代方案。如果此屬性存在,SCXML 處理器必須在執(zhí)行父 <send> 元素時對其進行執(zhí)行,并將結(jié)果視為已作為“type”值輸入 | |
| id | false | 不得與“idlocation”一起出現(xiàn) | xml:ID | none | 要用作此 <send> 實例的標識符的字符串文字 | |
| idlocation | false | 不得與“id”一起出現(xiàn) | 位置表達式 | none | 任何位置表達式執(zhí)行為可以存儲系統(tǒng)生成的 id 的數(shù)據(jù)模型位置 | |
| delay | false | 不能與“delayexpr”或?qū)傩浴皌arget”具有值“_internal”一起出現(xiàn) | Duration.datatype | none | 指示處理器在分派消息之前應(yīng)等待多長時間 | |
| delayexpr | false | 不得出現(xiàn)在“delay”或?qū)傩浴皌arget”的值為“_internal”時 | 值表達式 | none | “delay”的動態(tài)替代方案。如果此屬性存在,SCXML 處理器必須在執(zhí)行父 <send> 元素時對其進行執(zhí)行,并將結(jié)果視為已作為“delay”值輸入 | |
| namelist | false | 不得與 <content> 元素一起指定 | 位置表達式列表 | none | 一個或多個數(shù)據(jù)模型位置的空格分隔列表,作為屬性/值對包含在消息中。 (位置的名稱是屬性,存儲在位置的值是值。) |
children 可以包含:
- <content>:可以出現(xiàn) 0 次或 1次。
- <param>:可以出現(xiàn) 0 次或多次。
符合標準的 SCXML 文檔必須準確指定“event”、“eventexpr”和 <content> 之一。符合標準的文檔不得在 <content> 中指定“namelist”或 <param>。
- SCXML 處理器必須包含 <param> 或 ‘namelist’ 提供的所有屬性和值,即使出現(xiàn)重復(fù)也是如此。
- 如果存在“idlocation”,SCXML 處理器必須在執(zhí)行父 <send> 元素時生成一個 id 并將其存儲在此位置。
- 如果通過“delay”或“delayexpr”指定延遲,SCXML 處理器必須將字符串解釋為時間間隔。 它必須僅在延遲間隔過去后才發(fā)送消息。 (請注意,發(fā)送標記的執(zhí)行將立即返回。)處理器必須在執(zhí)行 <send> 元素時執(zhí)行所有參數(shù)到 <send>,而不是在實際發(fā)送消息時執(zhí)行。 如果 參數(shù)的執(zhí)行產(chǎn)生錯誤,處理器必須丟棄該消息而不嘗試傳遞它。 如果 SCXML 會話在延遲間隔過去之前終止,則 SCXML 處理器必須丟棄該消息而不嘗試傳遞它。
對應(yīng) XState 的 actions.send 函數(shù)。結(jié)構(gòu)類似:
{"event": "scxml.event","options": {"id": "scxml.id","delay": "scxml.delay","to": "scxml.target"} }示例:
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="狀態(tài)A"><state id="狀態(tài)A"><transition target="狀態(tài)B" event="跳轉(zhuǎn)"></transition><onentry><send event="跳轉(zhuǎn)" /></onentry></state><state id="狀態(tài)B"></state> </scxml> Machine({id: "狀態(tài)機",initial: "狀態(tài)A",states: {狀態(tài)A: {onEntry: actions.send("跳轉(zhuǎn)"),on: {跳轉(zhuǎn): "狀態(tài)B",},},狀態(tài)B: {},}, });3.4.2 <cancel>
<cancel> 元素用于取消延遲的 <send> 事件。 SCXML 處理器不得允許 <cancel> 影響未在同一會話中引發(fā)的事件。 處理器應(yīng)盡最大努力取消具有指定 ID 的所有延遲事件。 但是請注意,它不能保證成功,例如,如果事件在 <cancel> 標記執(zhí)行時已經(jīng)交付。
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="狀態(tài)A"><state id="狀態(tài)A"><onentry><cancel sendid="跳轉(zhuǎn)ID" /></onentry></state> </scxml>屬性字段描述如下:
| sendid | false | 不得與 sendideexpr 一起出現(xiàn) | IDREF | none | 延遲事件的 sendid | 要取消的事件的 ID。如果多個延遲事件有這個 sendid,處理器將全部取消 |
| sendidexpr | false | 不得與 sendid 一起出現(xiàn) | 值表達式 | none | 計算結(jié)果為延遲事件 ID 的任何表達式 | ‘sendid’ 的動態(tài)替代方案。如果此屬性存在,SCXML 處理器必須在執(zhí)行父 <cancel> 元素時對其進行執(zhí)行,并將結(jié)果視為已作為“sendid”的值輸入 |
對應(yīng) XState 的 actions.cancel 函數(shù)。
示例:
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="未登錄"><state id="未登錄"><transition target="已登錄" event="登錄"></transition></state><state id="已登錄"><transition target="已登錄" event="活動"></transition><onentry><send event="注銷" delay="1000 * 60" target="未登錄" id="消息ID" /></onentry><onexit><cancel sendid="消息ID"></cancel></onexit></state> </scxml> Machine({id: "狀態(tài)機",initial: "未登錄",states: {未登錄: {on: {登錄: "已登錄",},},已登錄: {onEntry: actions.send("注銷", {delay: 1000 * 60,id: "消息ID",}),onExit: actions.cancel("消息ID"),on: {注銷: "未登錄",活動: "已登錄",},},}, });也可以使用 XState 的 after 語法糖來實現(xiàn):
Machine({id: "狀態(tài)機",initial: "未登錄",states: {未登錄: {on: {登錄: "已登錄",},},已登錄: {after: {[1000 * 60]: "未登錄",},on: {活動: "已登錄",},},}, });3.4.3 <invoke>
<invoke> 元素用于創(chuàng)建外部服務(wù)的實例。
<invoke id="ID_SUB" src="sub.scxml"><param expr="3" name="i_ID" /> </invoke><invoke> 提供了一種更緊密耦合的通信形式,特別是能夠觸發(fā)平臺定義的服務(wù)并將數(shù)據(jù)傳遞給它。 它及其子 <finalize> 在模擬外部服務(wù)行為的狀態(tài)中很有用。 <invoke> 元素在狀態(tài)的 <onentry> 元素之后執(zhí)行,并導(dǎo)致創(chuàng)建外部服務(wù)的實例。 <param> 和 <content> 元素以及 ‘namelist’ 屬性可用于將數(shù)據(jù)傳遞給服務(wù)。
當并行狀態(tài)同時調(diào)用相同的外部服務(wù)時,將啟動外部服務(wù)的單獨實例。 它們可以通過與它們相關(guān)聯(lián)的 id 來區(qū)分。 類似地,從外部服務(wù)返回的事件中包含的 id 可用于確定哪些事件是對哪些調(diào)用的響應(yīng)。 返回的每個事件將僅由調(diào)用它的狀態(tài)中的 <finalize> 處理,但該事件隨后會像狀態(tài)機接收的任何其他事件一樣被處理。 因此,finalize 代碼可以被認為是在將事件添加到事件隊列之前應(yīng)用的預(yù)處理階段。 請注意,該事件將傳遞給所有并行狀態(tài)以檢查轉(zhuǎn)換。
由于當狀態(tài)機離開調(diào)用狀態(tài)時調(diào)用將被取消,因此在將立即退出的狀態(tài)下開始調(diào)用是沒有意義的。 因此,<invoke> 元素在進入狀態(tài)時執(zhí)行,但僅在檢查無事件轉(zhuǎn)換和未決內(nèi)部事件驅(qū)動的轉(zhuǎn)換之后。 如果找到任何此類啟用的轉(zhuǎn)換,則立即執(zhí)行該轉(zhuǎn)換并立即退出該狀態(tài),而不會觸發(fā)調(diào)用。 因此,只有在狀態(tài)機達到穩(wěn)定配置時才會觸發(fā)調(diào)用,即在等待外部事件時它將停留在其中的配置。
屬性字段描述如下:
| type | false | 不能與“typeexpr”屬性一起出現(xiàn) | URI | none | 指定外部服務(wù)類型的 URI | |
| typeexpr | false | 不得與“type”屬性一起出現(xiàn) | 值表達式 | none | 計算結(jié)果為 URI 的任何值表達式,該 URI 將是 ‘type’ 的有效值 | “type”的動態(tài)替代方案。如果此屬性存在,SCXML 處理器必須在執(zhí)行父 <invoke> 元素時對其進行執(zhí)行,并將結(jié)果視為已作為“type”的值輸入 |
| src | false | URI | none | 要傳遞給外部服務(wù)的 URI | ||
| srcexpr | false | 值表達式 | none | ‘src’ 的動態(tài)替代方案。如果此屬性存在,SCXML 處理器必須在執(zhí)行父 <invoke> 元素時對其進行執(zhí)行,并將結(jié)果視為已作為“src”的值輸入 | ||
| id | false | ID | none | 要用作此 <invoke> 實例的標識符的字符串文字 | ||
| idlocation | false | 位置表達式 | none | 任何對數(shù)據(jù)模型位置求值的數(shù)據(jù)模型表達式 | ||
| namelist | false | 位置表達式列表 | none | 要作為屬性/值對傳遞給調(diào)用服務(wù)的一個或多個數(shù)據(jù)模型位置的空格分隔列表。 (位置的名稱是屬性,位置存儲的值是值。) | ||
| autoforward | false | 布爾值 | false | 指示是否將事件轉(zhuǎn)發(fā)到調(diào)用的進程的標志 |
children 可以包含:
- <content>:可以出現(xiàn) 0 次或 1次。
- <param>:可以出現(xiàn) 0 次或多次。
- <finalize>:可以出現(xiàn) 0 次或 1次。
當 autoforward 屬性設(shè)置為 true 時,SCXML 處理器必須將它接收到的每個外部事件的精確副本發(fā)送到調(diào)用的進程。SCXML 處理器必須在將事件從調(diào)用會話的外部事件隊列中刪除以進行處理時轉(zhuǎn)發(fā)該事件。
外部服務(wù)在處理時可能會返回多個事件。如果 <invoke> 實例中有一個 <finalize> 處理程序創(chuàng)建了生成事件的服務(wù),則 SCXML 處理器必須在從事件隊列中刪除事件以進行處理之前立即執(zhí)行該 <finalize> 處理程序中的代碼。它絕不能在 <invoke> 的任何其他實例中執(zhí)行 <finalize> 處理程序。一旦外部服務(wù)完成處理,它必須返回一個特殊事件 done.invoke.id 到調(diào)用進程的外部事件隊列,其中id是對應(yīng) <invoke> 元素的調(diào)用ID。外部服務(wù)不得 在此完成事件之后生成任何其他事件。
<invoke> 的實現(xiàn)
包括父進程和子進程之間的通信,是特定于平臺的,但是在被調(diào)用的進程本身是 SCXML 會話的情況下,以下要求成立:
- 如果 <invoke> 中的 <param> 元素的 name 與調(diào)用會話的頂級數(shù)據(jù)聲明中的 <data> 元素的 id 匹配,則 SCXML 處理器必須使用 <param > 元素作為相應(yīng) <data> 元素的初始值。(頂級數(shù)據(jù)聲明是包含在 <scxml> 子元素的 <datamodel> 元素中的那些聲明。)(請注意,這意味著在 <data> 元素中指定的任何值都將被忽略。) namelist 類似。如果名稱列表中鍵的值與調(diào)用會話的頂級數(shù)據(jù)模型中的 <data> 元素的 id 匹配, scxml 處理器必須使用鍵的值作為相應(yīng) <data> 元素的初始值。如果名稱不匹配,處理器不得 將 <param> 元素或名稱列表鍵/值對的值添加到調(diào)用會話的數(shù)據(jù)模型中。但是,處理器可以通過其他一些特定于平臺的方式使這些值可用。
- 當被調(diào)用的狀態(tài)機達到頂級最終狀態(tài)時,處理器必須放置事件 done.invoke.id 上調(diào)用機,其中所述外部事件隊列 ID 是用于此調(diào)用的調(diào)用ID。請注意,達到頂級最終狀態(tài)對應(yīng)于機器的正常終止,并且一旦處于此狀態(tài),它就無法生成或處理任何進一步的事件。
- 如上所述,如果調(diào)用狀態(tài)機在接收到 done.invoke 之前退出包含調(diào)用的狀態(tài)。id事件,它取消調(diào)用的會話。執(zhí)行此操作的方法是特定于平臺的。然而,當它被取消時,被調(diào)用的會話必須在下一個微步結(jié)束時退出。處理器必須為被調(diào)用會話中的所有活動狀態(tài)執(zhí)行 處理程序,但它不能 生成 done.invoke.id 事件。一旦取消調(diào)用的會話,處理器必須忽略它從該會話接收到的任何事件。特別是它絕不能將它們插入到調(diào)用會話的外部事件隊列中。
- SCXML 處理器必須支持使用 SCXML 事件I/O 處理器在調(diào)用會話和被調(diào)用會話之間進行通信。處理器可以支持使用其他事件I/O 處理器在調(diào)用會話和被調(diào)用會話之間進行通信。
對應(yīng) XState State Node 的 invoke 屬性。描述如下:
{"id": "","src": "","autoForward": false,"data": {},"onDone": {},"onError": {} }示例:
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="等待中"><state id="等待中"><transition event="done.invoke.子狀態(tài)機" target="時間到" /><invoke id="子狀態(tài)機" type="http://www.w3.org/TR/scxml/"><content><scxml name="分鐘子狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="等待中" initial="激活中"><state id="激活中"><onentry><send delay="60s" event="結(jié)束"></send></onentry><transition target="完成" event="結(jié)束"></transition></state><final id="完成"></final></scxml></content></invoke></state><final id="時間到"></final> </scxml> const minuteMachine = Machine({id: "分鐘子狀態(tài)機",initial: "激活中",states: {激活中: {after: {60000: { target: "完成" },},},完成: { type: "final" },}, });Machine({id: "狀態(tài)機",initial: "等待中",states: {等待中: {invoke: {src: minuteMachine,onDone: "時間到",},},時間到: {type: "final",},}, });3.4.4 <finalize>
<finalize> 元素使調(diào)用會話能夠使用被調(diào)用會話返回的事件中包含的數(shù)據(jù)更新其數(shù)據(jù)模型。
<finalize> 包含在執(zhí)行 <invoke> 后外部服務(wù)返回事件時執(zhí)行的可執(zhí)行內(nèi)容。 在系統(tǒng)查找與事件匹配的轉(zhuǎn)換之前應(yīng)用此內(nèi)容。 在可執(zhí)行內(nèi)容中,系統(tǒng)變量“_event”可用于引用正在處理的事件中包含的數(shù)據(jù)。在并行狀態(tài)的情況下,僅執(zhí)行原始調(diào)用狀態(tài)下的finalize代碼。
在調(diào)用期間狀態(tài)機從被調(diào)用組件接收到的任何事件都由 <finalize> 處理程序在選擇轉(zhuǎn)換之前進行預(yù)處理。 finalize> 代碼用于規(guī)范化返回數(shù)據(jù)的形式,并在執(zhí)行轉(zhuǎn)換的“event”和“cond”子句之前更新數(shù)據(jù)模型。
在符合的SCXML文件,在<敲定>的可執(zhí)行內(nèi)容不得引發(fā)事件或調(diào)用外部動作。特別是,<send> 和 <raise> 元素 不得出現(xiàn)。
children 可以包含 可執(zhí)行內(nèi)容。
XState 沒有對應(yīng)的 API,XState 對于處理消息是非常靈活的,所以這一塊能力是內(nèi)置進去的。
示例:
<scxml name="狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="等待中"><datamodel><data expr="1" id="VarValue" /></datamodel><state id="等待中"><transition event="childToParent" cond="VarValue==2" target="結(jié)束" /><invoke id="子狀態(tài)機" type="http://www.w3.org/TR/scxml/"><content><scxml name="發(fā)送消息到父級子狀態(tài)機" version="1.0" xmlns="http://www.w3.org/2005/07/scxml" initial="完成" initial="激活中"><final id="完成"><onentry><send target="#_parent" event="childToParent"><param name="aParam" expr="2" /></send></onentry></final></scxml></content><finalize><assign location="VarValue" expr="_event.data.aParam"/></finalize></invoke></state><final id="結(jié)束"></final> </scxml>3.5 未對應(yīng) XState API
上面按照規(guī)范與 XState 進行了對應(yīng)。還有一部分是 XState 特色產(chǎn)物。如下:
- Actor:Actor 模型,一套非常成熟的模型。用來擴展子狀態(tài)機。
- Interpreter:由于 XState 的狀態(tài)機是一套純函數(shù)編寫,無任何副作用。所以官方提供了一個 Interpreter 用來托管副作用。
- Model:用來改善開發(fā)人員體驗,分離和組織 context 和 event,共享模型。
3.6 對應(yīng)大圖
整個對應(yīng)關(guān)系,大致如下圖所示:
4. 最后
連續(xù)熬了一個多星期的夜,對 SCXML 和 XState 的關(guān)系進行了梳理和對齊,最終產(chǎn)出了這篇 4萬 多字的文章。
做為一個 XState 的 “過來人”,這篇從規(guī)范到工具對應(yīng)關(guān)系的文章,正是當初那個在入門產(chǎn)生疑惑時的我,最需要的東西。
也希望這篇文章可以幫助入門和使用狀態(tài)機及 XState 的同學(xué)解除部分疑惑。
? Github 文章地址
總結(jié)
以上是生活随笔為你收集整理的前端状态机系列:SCXML与XState对应关系的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SQLite编译问题
- 下一篇: 网页设计引入的字体包过大的压缩方法