本期精讀的是有限狀態機管理工具 robot 源碼。前端
有限狀態機是指有限個數的狀態之間相互切換的數學模型,在業務與遊戲開發中有限狀態都很常見,包括髮請求也是一種有限狀態機的模型。react
筆者將在簡介中介紹這個庫的使用方式,在精讀中介紹實現原理,最後總結在業務中使用的價值。git
這個庫的核心就是利用 createMachine
建立一個有限狀態機:github
import { createMachine, state, transition } from 'robot3';
const machine = createMachine({
inactive: state(
transition('toggle', 'active')
),
active: state(
transition('toggle', 'inactive')
)
});
export default machine;
複製代碼
如上圖所示,咱們建立了一個有限狀態機 machine
,包含了兩種狀態:inactive
與 active
,而且能夠經過 toggle
動做在兩種狀態間作切換。typescript
與 React 結合則有 react-robot:後端
import { useMachine } from 'react-robot';
import React from 'react';
import machine from './machine'
function App() {
const [current, send] = useMachine(machine);
return (
<button type="button" onClick={() => send('toggle')}>
State: {current.name}
</button>
)
}
複製代碼
經過 useMachine
拿到的 current.name
表示當前狀態值,send
用來發送改變狀態的指令。安全
至於爲何要用有限狀態機管理工具,官方文檔舉了個例子 - 點擊編輯後進入編輯態,點擊保存後返回原始狀態的例子:微信
點擊 Edit 按鈕後,將進入下圖的狀態,點擊 Save 後若是輸入的內容校驗經過保存後再回到初始狀態:框架
若是不用有限狀態機,咱們首先會建立兩個變量存儲是否處於編輯態,以及當前輸入文本是什麼:ide
let editMode = false;
let title = '';
複製代碼
若是再考慮和後端的交互,就會增長三個狀態 - 保存中、校驗、保存是否成功:
let editMode = false;
let title = '';
let saving = false;
let validating = false;
let saveHadError = false;
複製代碼
就算使用 React、Vue 等框架數據驅動 UI,咱們仍是免不了對複雜狀態進行管理。若是使用有限狀態機實現,將是這樣的:
import { createMachine, guard, immediate, invoke, state, transition, reduce } from 'robot3';
const machine = createMachine({
preview: state(
transition('edit', 'editMode',
// Save the current title as oldTitle so we can reset later.
reduce(ctx => ({ ...ctx, oldTitle: ctx.title }))
)
),
editMode: state(
transition('input', 'editMode',
reduce((ctx, ev) => ({ ...ctx, title: ev.target.value }))
),
transition('cancel', 'cancel'),
transition('save', 'validate')
),
cancel: state(
immediate('preview',
// Reset the title back to oldTitle
reduce(ctx => ({ ...ctx, title: ctx.oldTitle })
)
),
validate: state(
// Check if the title is valid. If so go
// to the save state, otherwise go back to editMode
immediate('save', guard(titleIsValid)),
immediate('editMode')
)
save: invoke(saveTitle,
transition('done', 'preview'),
transition('error', 'error')
),
error: state(
// Should we provide a retry or...?
)
});
複製代碼
其中 immediate
表示直接跳到下一個狀態,reduce
則能夠對狀態機內部數據進行拓展。好比 preview
返回了 oldTitle
,那麼 cancle
時就能夠經過 ctx.oldTitle
拿到;invoke
表示調用第一個函數後,再執行 state
。
經過上面的代碼咱們能夠看到使用狀態機的好處:
preview
只能切換到 edit
狀態,這樣就算在錯誤的狀態發錯指令也不會產生異常狀況。robot 重要的函數有 createMachine, state, transition, immediate
,下面一一拆解說明。
createMachine 表示建立狀態機:
export function createMachine(current, states, contextFn = empty) {
if(typeof current !== 'string') {
contextFn = states || empty;
states = current;
current = Object.keys(states)[0];
}
if(d._create) d._create(current, states);
return create(machine, {
context: valueEnumerable(contextFn),
current: valueEnumerable(current),
states: valueEnumerable(states)
});
}
複製代碼
能夠看到,若是傳遞了一個對象,經過 Object.keys(states)[0]
拿到第一個狀態做爲當前狀態(標記在 current
),最終將保存三個屬性:
context
當前狀態機內部屬性,初始化是空的。current
當前狀態。states
全部狀態,也就是 createMachine
傳遞的第一個參數。再看 create
函數:
let create = (a, b) => Object.freeze(Object.create(a, b));
複製代碼
也就是建立了一個不修改的對象做爲狀態機。
這個是 machine
對象:
let machine = {
get state() {
return {
name: this.current,
value: this.states[this.current]
};
}
};
複製代碼
也就是說,狀態機內部的狀態管理是經過對象完成的,並提供了 state()
函數拿到當前的狀態名和狀態值。
state 用來描述狀態支持哪些轉換:
export function state(...args) {
let transitions = filter(transitionType, args);
let immediates = filter(immediateType, args);
let desc = {
final: valueEnumerable(args.length === 0),
transitions: valueEnumerable(transitionsToMap(transitions))
};
if(immediates.length) {
desc.immediates = valueEnumerable(immediates);
desc.enter = valueEnumerable(enterImmediate);
}
return create(stateType, desc);
}
複製代碼
transitions
與 immediates
表示從 args
裏拿到 transition
或 immediate
的結果。
方法是經過以下方式定義 transition
與 immediate
:
export let transition = makeTransition.bind(transitionType);
export let immediate = makeTransition.bind(immediateType, null);
function filter(Type, arr) {
return arr.filter(value => Type.isPrototypeOf(value));
}
複製代碼
那麼若是一個函數是經過 immediate
建立的,就能夠經過 immediateType.isPrototypeOf()
的校驗,此方法適用範圍很廣,在任何庫裏均可以用來校驗拿到對應函數建立的對象。
若是參數數量爲 0,表示這個狀態是最終態,沒法進行轉換。最後經過 create
建立一個對象,這個對象就是狀態的值。
transition 是寫在 state
中描述當前狀態能夠如何變換的函數,其實際函數是 makeTransistion
:
function makeTransition(from, to, ...args) {
let guards = stack(filter(guardType, args).map(t => t.fn), truthy, callBoth);
let reducers = stack(filter(reduceType, args).map(t => t.fn), identity, callForward);
return create(this, {
from: valueEnumerable(from),
to: valueEnumerable(to),
guards: valueEnumerable(guards),
reducers: valueEnumerable(reducers)
});
}
複製代碼
因爲:
export let transition = makeTransition.bind(transitionType);
export let immediate = makeTransition.bind(immediateType, null);
複製代碼
可見 from
爲 null
即表示當即轉換到狀態 to
。transition
最終返回一個對象,其中 guards
是從 transition
或 immediate
參數中找到的,由 guards
函數建立的對象,當這個對象回調函數執行成功時此狀態才生效。
...args
對應 transition('toggle', 'active')
或 immediate('save', guard(titleIsValid))
,而 stack(filter(guardType, args).map(t => t.fn), truthy, callBoth)
這句話就是從 ...args
中尋找是否有 guards
,reducers
同理。
最後看看狀態是如何改變的,設置狀態改變的函數是 transitionTo:
function transitionTo(service, fromEvent, candidates) {
let { machine, context } = service;
for(let { to, guards, reducers } of candidates) {
if(guards(context)) {
service.context = reducers.call(service, context, fromEvent);
let original = machine.original || machine;
let newMachine = create(original, {
current: valueEnumerable(to),
original: { value: original }
});
let state = newMachine.state.value;
return state.enter(newMachine, service, fromEvent);
}
}
}
複製代碼
能夠看到,若是存在 guards
,則須要在 guards
執行返回成功時才能夠正確改變狀態。同時 reducers
能夠修改 context
也在 service.context = reducers.call(service, context, fromEvent);
這一行體現了出來。最後經過生成一個新的狀態機,並將 current
標記爲 to
。
最後咱們看 state.enter
這個函數,這個函數在 state 函數中有定義,其本質是繼承了 stateType
:
let stateType = { enter: identity };
複製代碼
而 identity
這個函數就是當即執行函數:
let identity = a => a;
複製代碼
所以至關於返回了新的狀態機。
有限狀態機相比普通業務描述,實際上是增長了一些狀態間轉化的約束來達到優化狀態管理的目的,而且狀態描述也會更規範一些,在業務中具備必定的實用性。
固然並非全部業務都適用有限狀態機,由於新框架仍是有一些學習成本要考慮。最後經過源碼的學習,咱們又瞭解到一些新的框架級小技巧,能夠靈活應用到本身的框架中。
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)