1. 程式人生 > 其它 >robot ride edit 頁面不顯示_精讀《robot 原始碼 - 有限狀態機》

robot ride edit 頁面不顯示_精讀《robot 原始碼 - 有限狀態機》

技術標籤:robot ride edit 頁面不顯示

1 概述

本期精讀的是有限狀態機管理工具 robot 原始碼。

有限狀態機是指有限個數的狀態之間相互切換的數學模型,在業務與遊戲開發中有限狀態都很常見,包括髮請求也是一種有限狀態機的模型。

筆者將在簡介中介紹這個庫的使用方式,在精讀中介紹實現原理,最後總結在業務中使用的價值。

2 簡介

這個庫的核心就是利用 createMachine 建立一個有限狀態機:

import { createMachine, state, transition } from 'robot3';

const machine = createMachine({
  inactive: state(
    transition('toggle', 'active')
  ),
  active: state(
    transition('toggle', 'inactive')
  )
});

export default machine;

如上圖所示,我們建立了一個有限狀態機 machine,包含了兩種狀態:inactiveactive,並且可以通過 toggle 動作在兩種狀態間做切換。

與 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 用來發送改變狀態的指令。

至於為什麼要用有限狀態機管理工具,官方文件舉了個例子 - 點選編輯後進入編輯態,點選儲存後返回原始狀態的例子:

36aeb5f865efdbd1dbe897fc5af8ce08.png

點選 Edit 按鈕後,將進入下圖的狀態,點選 Save 後如果輸入的內容校驗通過儲存後再回到初始狀態:

23711a4371de0719fc52861e1fc1e085.png

如果不用有限狀態機,我們首先會建立兩個變數儲存是否處於編輯態,以及當前輸入文字是什麼:

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

通過上面的程式碼我們可以看到使用狀態機的好處:

  1. 狀態清晰,先羅列出某個業務邏輯的全部狀態,避免遺漏。
  2. 狀態轉換安全。比如 preview 只能切換到 edit 狀態,這樣就算在錯誤的狀態發錯指令也不會產生異常情況。

3 精讀

robot 重要的函式有 createMachine, state, transition, immediate,下面一一拆解說明。

createMachine

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

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);
}

transitionsimmediates 表示從 args 裡拿到 transitionimmediate 的結果。

方法是通過如下方式定義 transitionimmediate:

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

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);

可見 fromnull 即表示立即轉換到狀態 totransition 最終返回一個物件,其中 guards 是從 transitionimmediate 引數中找到的,由 guards 函式建立的物件,當這個物件回撥函式執行成功時此狀態才生效。

...args 對應 transition('toggle', 'active')immediate('save', guard(titleIsValid)),而 stack(filter(guardType, args).map(t => t.fn), truthy, callBoth) 這句話就是從 ...args 中尋找是否有 guardsreducers 同理。

最後看看狀態是如何改變的,設定狀態改變的函式是 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;

因此相當於返回了新的狀態機。

4 總結

有限狀態機相比普通業務描述,其實是增加了一些狀態間轉化的約束來達到優化狀態管理的目的,並且狀態描述也會更規範一些,在業務中具有一定的實用性。

當然並不是所有業務都適用有限狀態機,因為新框架還是有一些學習成本要考慮。最後通過原始碼的學習,我們又瞭解到一些新的框架級小技巧,可以靈活應用到自己的框架中。

討論地址是:精讀《robot 原始碼 - 有限狀態機》 · Issue #209 · dt-fe/weekly

如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公眾號

9bd59e157da9e1f152fe7e25160fb0ed.png
版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)