利用 XState(有限狀態機) 編寫易於變動的代碼

目前來講,不管是 to c 業務,仍是 to b 業務,對於前端開發者的要求愈來愈高,各類絢麗的視覺效果,複雜的業務邏輯層出不窮。針對於業務邏輯而言,貫穿後端業務和前端交互都有一個關鍵點 —— 狀態轉換。javascript

固然了,這種代碼實現自己並不複雜,真正的難點在於如何快速的進行代碼的修改。html

在實際開發項目的過程當中,ETC 原則,即 Easier To Change,易於變動是很是重要的。爲何解耦很好? 爲何單一職責頗有用? 爲何好的命名很重要?由於這些設計原則讓你的代碼更容易發生變動。ETC 甚至能夠說是其餘原則的基石,能夠說,咱們如今所做的一切都是爲了更容易變動!!特別是針對於初創公司,更是如此。前端

例如:項目初期,當前的網頁有一個模態框,能夠進行編輯,模態框上有兩個按鈕,保存與取消。這裏就涉及到模態框的顯隱狀態以及權限管理。隨着時間的推移,需求和業務發生了改變。當前列表沒法展現該項目的全部內容,在模態框中咱們不但須要編輯數據,同時須要展現數據。這時候咱們還須要管理按鈕之間的聯動。僅僅這些就較爲複雜,更不用說涉及多個業務實體以及多角色之間的細微控制。java

從新審視自身代碼,雖然以前咱們作了大量努力利用各類設計原則,可是想要快速而安全的修改散落到各個函數中的狀態修改,仍是很是浪費心神的,並且還很容易出現「漏網之魚」。react

這時候,咱們不只僅須要依靠自身經驗寫好代碼,同時也須要一些工具的輔助。git

有限狀態機

有限狀態機是一個很是有用的數學計算模型,它描述了在任何給定時間只能處於一種狀態的系統的行爲。固然,該系統中只可以創建出一些有限的、定性的「模式」或「狀態」 ,並不描述與該系統相關的全部(多是無限的)數據。例如,水能夠是四種狀態中的一種: 固體(冰)、液體、氣體或等離子體。然而,水的溫度能夠變化,它的測量是定量的和無限的。github

總結來講,有限狀態機的三個特徵爲:編程

  • 狀態總數(state)是有限的。
  • 任一時刻,只處在一種狀態之中。
  • 某種條件下,會從一種狀態轉變(transition)到另外一種狀態。

在實際開發中,它還須要:後端

  • 初始狀態
  • 觸發狀態變化的事件和轉換函數
  • 最終狀態的集合(有多是沒有最終狀態)

先看一個簡單的紅綠燈狀態轉換:設計模式

const light = {
  currentState: 'green',
  
  transition: function () {
    switch (this.currentState) {
      case "green":
        this.currentState = 'yellow'
        break;
      case "yellow":
        this.currentState = 'red'
        break;
      case "red": 
        this.currentState = 'green'
        break;
      default:
        break;
    }
  }
}

有限狀態機在遊戲開發中大放異彩,已經成爲了一種經常使用的設計模式。用這種方式可使每個狀態都是獨立的代碼塊,與其餘不一樣的狀態分開獨立運行,這樣很容易檢測遺漏條件和移除非法狀態,減小了耦合,提高了代碼的健壯性,這麼作可使得遊戲的調試變得更加方便,同時也更易於增長新的功能。

對於前端開發來講,咱們能夠從其餘工程領域中多年使用的經驗學習與再創造。

XState 體驗

實際上開發一個 簡單的狀態機並非特別複雜的事情,可是想要一個完善,實用性強,還具備可視化工具的狀態機可不是一個簡單的事。

這裏我要推薦 XState,該庫用於建立、解釋和執行有限狀態機和狀態圖。

簡單來講:上述的代碼能夠這樣寫。

import { Machine } from 'xstate'

const lightMachine = Machine({
  // 識別 id, SCXML id 必須惟一
  id: 'light',
  // 初始化狀態,綠燈
  initial: 'green',
  
  // 狀態定義 
  states: {
    green: {
      on: {
        // 事件名稱,若是觸發 TIMRE 事件,直接轉入 yellow 狀態
        TIMRE: 'yellow'
      }
    },
    yellow: {
      on: {
        TIMER: 'red'
      }
    },
    red: {
      on: {
        TIMER: 'green'
      }
    }
  }
})

// 設置當前狀態
const currentState = 'green'

// 轉換的結果
const nextState = lightMachine.transition(currentState, 'TIMER').value 
// => 'yellow'

// 若是傳入的事件沒有定義,則不會發生轉換,若是是嚴格模式,將會拋出錯誤
lightMachine.transition(currentState, 'UNKNOWN').value

其中 SCXML 是狀態圖可擴展標記語言, XState 遵循該標準,因此須要提供 id。當前狀態機也能夠轉換爲 JSON 或 SCXML。

雖然 transition 是一個純函數,很是好用,可是在真實環境使用狀態機,咱們仍是須要更強大的功能。如:

  • 跟蹤當前狀態
  • 執行反作用
  • 處理延遲過分以及時間
  • 與外部服務溝通

XState 提供了 interpret 函數,

import { Machine,interpret } from 'xstate'

// 。。。 lightMachine 代碼

// 狀態機的實例成爲 serivce
const lightService = interpret(lightMachine)
   // 當轉換時候,觸發的事件(包括初始狀態)
  .onTransition(state => {
    // 返回是否改變,若是狀態發生變化(或者 context 以及 action 後文提到),返回 true 
    console.log(state.changed) 
    console.log(state.value)
  })
  // 完成時候觸發
  .onDone(() => {
    console.log('done')
  })

// 開啓
lightService.start()

// 將觸發事件改成 發送消息,更適合狀態機風格
// 初始化狀態爲 green 綠色
lightService.send('TIMER') // yellow
lightService.send('TIMER') // red

// 批量活動
lightService.send([
  'TIMER',
  'TIMER'
])

// 中止
lightService.stop()

// 從特定狀態啓動當前服務,這對於狀態的保存以及使用更有做用
lightService.start(previousState)

咱們也能夠結合其餘庫在 Vue React 框架中使用,僅僅只用幾行代碼就實現了咱們想要的功能。

import lightMachine from '..'
// react hook 風格
import { useMachine } from '@xstate/react'

function Light() {
  const [light, send] = useMachine(lightMachine)
  
  return <>
    // 當前狀態 state 是不是綠色
    <span>{light.matches('green') && '綠色'}</span>    
    // 當前狀態的值
    <span>{light.value}</span>  
    // 發送消息
    <button onClick={() => send('TIMER')}>切換</button>
  </>
}

當前的狀態機也是還能夠進行嵌套處理,在紅燈狀態下添加人的行動狀態。

import { Machine } from 'xstate';

const pedestrianStates = {
  // 初識狀態 行走
  initial: 'walk',
  states: {
    walk: {
      on: {
        PED_TIMER: 'wait'
      }
    },
    wait: {
      on: {
        PED_TIMER: 'stop'
      }
    },
    stop: {}
  }
};

const lightMachine = Machine({
  id: 'light',
  initial: 'green',
  states: {
    green: {
      on: {
        TIMER: 'yellow'
      }
    },
    yellow: {
      on: {
        TIMER: 'red'
      }
    },
    red: {
      on: {
        TIMER: 'green'
      },
      ...pedestrianStates
    }
  }
});

const currentState = 'yellow';

const nextState = lightMachine.transition(currentState, 'TIMER').value;

// 返回級聯對象 
// => {
//   red: 'walk'
// }

// 也能夠寫爲 red.walk
lightMachine.transition('red.walk', 'PED_TIMER').value;

// 轉化後返回
// => {
//   red: 'wait'
// }

// TIMER 還能夠返回下一個狀態
lightMachine.transition({ red: 'stop' }, 'TIMER').value;
// => 'green'

固然了,既然有嵌套狀態,咱們還能夠利用 type: 'parallel' ,進行串行和並行處理。

除此以外,XState 還有擴展狀態 context 和過分防禦 guards。這樣的話,更可以模擬現實生活

// 是否能夠編輯
functions canEdit(context: any, event: any, { cond }: any) {
  console.log(cond)
  // => delay: 1000
  
  // 是否有某種權限 ???
  return hasXXXAuthority(context.user)
}


const buttonMachine = Machine({
  id: 'buttons',
  initial: 'green',
  // 擴展狀態,例如 用戶等其餘全局數據
  context: {
    // 用戶數據
    user: {}
  },
  states: {
    view: {
      on: {
        // 對應以前 TIMRE: 'yellow'
        // 實際上 字符串沒法表達太多信息,須要對象表示
        EDIT: {
          target: 'edit',
          // 若是沒有該權限,不進行轉換,處於原狀態
          // 若是沒有附加條件,直接 cond: searchValid
          cond: {
            type: 'searchValid',
            delay: 3
          }
        }, 
      }
    }
  }
}, {
  // 守衛
  guards: {
    canEdit,
  }
})


// XState 給予了更加合適的 API 接口,開發時候 Context 可能不存在
// 或者咱們須要在不一樣的上下文 context 中複用狀態機,這樣代碼擴展性更強
const buttonMachineWithDelay = buttonMachine.withContext({
  user: {},
  delay: 1000
})

// withContext 是直接替換,不進行淺層合併,可是咱們能夠手動合併
const buttonMachineWithDelay = buttonMachine.withContext({
  ...buttonMachine.context,
  delay: 1000
})

咱們還能夠經過瞬時狀態來過分,瞬態狀態節點能夠根據條件來肯定機器應從先前的狀態真正進入哪一個狀態。瞬態狀態表現爲空字符串,即 '',如

const timeOfDayMachine = Machine({
  id: 'timeOfDay',
  // 當前不知道是什麼狀態
  initial: 'unknown',
  context: {
    time: undefined
  },
  states: {
    // Transient state
    unknown: {
      on: {
        '': [
          { target: 'morning', cond: 'isBeforeNoon' },
          { target: 'afternoon', cond: 'isBeforeSix' },
          { target: 'evening' }
        ]
      }
    },
    morning: {},
    afternoon: {},
    evening: {}
  }
}, {
  guards: {
    isBeforeNoon: //... 確認當前時間是否小於 中午 
    isBeforeSix: // ... 確認當前時間是否小於 下午 6 點
  }
});

const timeOfDayService = interpret(timeOfDayMachine
  .withContext({ time: Date.now() }))
  .onTransition(state => console.log(state.value))
  .start();

timeOfDayService.state.value 
// 根據當前時間,能夠是 morning afternoon 和 evening,而不是 unknown 轉態

到這裏,我以爲已經介紹 XState 不少功能了,篇幅所限,不能徹底介紹全部功能,不過當前的功能已經足夠大部分業務需求使用了。若是有其餘更復雜的需求,能夠參考 XState 文檔

這裏列舉一些沒有介紹到的功能點:

  • 進入和離開某狀態觸發動做(action 一次性)和活動(activity 持續性觸發,直到離開某狀態)
  • 延遲事件與過分 after
  • 服務調用 invoke,包括 promise 以及 兩個狀態機之間相互交互
  • 歷史狀態節點,能夠經過配置保存狀態而且回退狀態

固然了,對比於 x-state 這種,還有其餘的狀態機工具,如 javascript-state-machine , Ego 等。你們能夠酌情考慮使用。

總結

對於現代框架而言,不管是如火如荼的 React Hook 仍是漸入佳境的 Vue Compoistion Api,其本質都想提高狀態邏輯的複用能力。可是考慮大部分場景下,狀態自己的切換都是有特定約束的,若是僅僅靠良好的編程習慣,恐怕仍是難以寫出抑鬱修改的代碼。而 FSM 以及 XState 無疑是一把利器。

鼓勵一下

若是你以爲這篇文章不錯,但願能夠給與我一些鼓勵,在個人 github 博客下幫忙 star 一下。

博客地址

參考

XState 文檔

JavaScript與有限狀態機

相關文章
相關標籤/搜索