使用有限狀態機管理狀態

背景

近年來因爲一些前端框架的興起然後逐漸成熟,組件化的概念已經深刻人心,爲了管理好大型應用中錯綜複雜的組件,又有了單向數據流的思想指引着咱們,Vuex、Redux、MobX等狀態管理工具也許你們都信手拈來。
咱們手握着這些工具,不斷思考着哪些數據應該放在全局,哪些數據應該局部消化,這樣那樣的數據該怎樣流轉。仔細想一想會發現,咱們一直在作的是如何將數據存在合理的地方,進而去規範怎樣使用這些數據,咱們稱之爲狀態管理,但我以爲好像只是作了狀態存儲與使用,卻沒有作好管理二字。嗯,總感受差了些什麼。javascript

你能將狀態描述清楚嗎?

來看一段簡單的代碼:html

state = {
    data: [
        {
            id: 1,
            userName: xxx
        },
        {
            id: 2,
            userName: yyy
        }
    ]
}

若是根據UI = f(state)來講,上面的state.data就是一個狀態,它能直接反映視圖的樣子:前端

render () {
    const {data} = this.state
    return (
        <div>
            {
                data && data.length ? data.map(item => <div :key={item.id}>{item.userName}</div>) : '暫無數據'
            }
        </div>
    )
}

咱們還會在合適的時機進行某種操做去更新狀態,好比請求獲取數據的接口就會去更新上面的data:vue

updateData () {
  getData().then(({data}) => {
    this.setState({data})
  })
}

但隨着時間的推移,這樣的狀態會愈來愈多,更新狀態的方法暗藏在日益膨脹的代碼中,維護者可能本身都要如履薄冰地一翻抽絲剝繭才勉強捋清楚狀態什麼時機更新,爲何要更新,更別說若是是去接盤一份祖傳代碼了。
究其緣由,我以爲是沒有將狀態描述清楚,更別說管理好狀態。因此一個描述得清楚的狀態是長什麼樣子呢?好比:開始的時候,data是空數組,初始化完成須要去更新data,增刪改完成後都須要更新data。
想一想平常,有沒有地方能一眼就看清楚這些狀態信息?需求文檔?UI稿?靠譜嗎?
圖片描述java

有限狀態機瞭解一下

本身動手豐衣足食,咱們的目標是在代碼裏就能清晰地看到這些狀態信息。若是咱們可以寫一份配置文件來將它們描述清楚,而後寫代碼的時候就根據這份配置文件來寫,有修改的時候也必須先修改這份配置文件,那咱們最後看配置文件就能對狀態信息一目瞭然了。
爲了達到這樣的目標,咱們得請有限狀態機來幫忙。概念性的東西請移步到JavaScript與有限狀態機,總的來講,有限狀態機是一個模型,它能描述清楚有哪些狀態,狀態之間是怎樣轉化的,它有如下特色:
1.狀態的數量是固定的
2.狀態會由於觸發了某種行爲而轉變成另外一種狀態(好比典型的promise,初始狀態爲pending,resolve後狀態轉變成fulfilled,reject則變成rejected)
3.任意時間點狀態惟一(初始化完成了才能進行增刪改嘛)
ok,瞭解這些以後,咱們來看看怎樣一步步達到目的。
咱們以一個需求爲例:
圖片描述
就是一個沒有一毛錢特效的Todoist,很是簡單的增刪改查。react

第一版

按照以前的想法,咱們首先須要一份配置文件來描述狀態:git

const machine = {
  // 初始狀態
  initial: "start",
  start: {
    INIT: "loadList"
  },
  loadList: {
    LOAD_LIST_SUCCESS: "showList",
    LOAD_LIST_ERROR: "showListError"
  },
  showListError: {
    RETRY: "loadList"
  },
  showList: {
    ADD: "add",
    EDIT: "edit",
    DELETE: "delete"
  },
  edit: {
    SAVE_EDIT: "saveEdit"
  },
  saveEdit: {
    SAVE_EDIT_SUCCESS: "loadList"
  },
  delete: {
    DELETE_SUCCESS: "loadList"
  },
  add: {
    ADD_SUCCESS: "loadList"
  }
};

配置是寫完了,如今對着上面的需求gif圖說一下這份配置是什麼意思。github

  1. 加載列表數據(initial: "start"表示初始狀態是start,start: {INIT: "loadList"}表示狀態start觸發INIT事件以後狀態會轉變成loadList)
  2. 加載列表數據失敗了(loadList觸發LOAD_LIST_ERROR事件狀態轉變爲showListError)
  3. 加載失敗後從新加載(showListError觸發RETRY事件以後狀態從新變回loadList
  4. 從新加載列表成功(loadList觸發LOAD_LIST_SUCCESS事件狀態轉變爲showList)
  5. 列表加載成功就能夠對列表進行增刪改操做(showList能夠觸發ADD、DELETE、EDIT事件對應增刪改操做帶來的狀態變化)

剩下的配置就不繼續寫了,能夠看到經過這份配置,咱們能夠清晰知道,這份代碼究竟作了些什麼,並且寫這份配置有利於整理好本身的思路,讓本身首先將需求過一遍,將全部邊邊角角經過寫配置預演一遍,而不是拿到需求就開擼,遇到了問題才發現以前寫的代碼不適用。同理若是需求有變,首先從這份配置入手,看看這波修改會對哪些狀態分支形成影響,就不會出現那種不知道改一個地方會不會影響到別的地方宛如拆炸彈同樣的心情。
接着,爲了方便根據這份配置來進行操做,須要實現一點輔助函數:數組

class App extends Component {
    constructor(props) {
        state = {
            curState: machine.initial
        }
    }
    handleNextState (nextState, action) {
        switch (nextState) {
            case "loadList":
            // 處理loadList的邏輯
            break;
        }
    }
    transition (action) {
        const { curState } = this.state;
        const nextState = machine[curState][action.type];
        if (nextState) {
            this.setState({ curState: nextState }, () =>
                this.handleNextState(nextState, action)
            );
        }
    }
}

基本就是這樣的結構,經過this.transition({ type: "INIT" })觸發一個事件(INIT)將當前狀態(start)轉變成另一個狀態(loadList),而handleNextState則處理狀態轉變後的邏輯(當狀態變成loadList須要去請求接口獲取列表數據)。經過這樣的方式,咱們真正將狀態管理了起來,由於咱們有清晰的配置文件去描述狀態,咱們有分層清晰的地方去處理當前狀態須要處理的邏輯,這就至關於有明確的戰略圖,你們都根據這份戰略圖各司其職作好本身的本分,這不是將狀態管理得層次分明嗎?
並且這樣作以後,比較容易規避一些意外的錯誤,由於任意時間點狀態惟一這個特色,帶來了狀態只能從一個狀態轉變到另外一個狀態,好比點擊一個按鈕提交,這時的狀態是提交中,咱們常常須要去處理用戶重複點擊而致使重複提交的事情:promise

let isSubmit = false
const submit = () => {
    if (isSubmit === true) return
    isSubmit = true
    toSubmit().then(() => isSubmit = false)
}
submit()

使用有限狀態機進行管理後就不須要寫這種額外的isSubmit狀態,由於提交中的狀態只能轉變爲提交完成。
上面的代碼完整版請點這裏

更進一步

雖然第一版對於狀態的管理更加清晰了一些,但仍然不夠直觀,若是能將配置轉化成圖就行了,有圖有真相嘛。心想事成:
圖片描述
不只有圖可看,還能夠逼真地將全部狀態都預演一遍。這個好東西就是xstate給予咱們的,它是一個實現有限狀態機模型的js庫,感興趣能夠去詳看,這裏咱們只須要按照它的寫法去寫狀態機的配置,就能夠生成出這樣的圖
看過xstate會發現,裏面的東西真很多,其實若是隻是想在簡單的項目上用這種模式試試水,卻要把整個庫引進來彷佛不太划算。那,不如本身來擼一個簡化版?
心動不如行動,先分析一下第一版有什麼不足之處。

  1. 沒有將有限狀態機的模式分離出來,若是不是用react而是用vue就用不了了。
  2. 沒有將模式分離出來致使複用性不好,總不能每一個地方要用的時候都要寫一次transition等方法吧。
  3. 配置項沒寫成xstate的樣子沒法使用xstate提供的工具生成圖。

如今首要的任務就是把有限狀態機的模式抽離出來,順便使用xstate的寫法來寫配置。

const is = (type, val) =>
  Object.prototype.toString.call(val) === "[object " + type + "]";

export class Fsm {
  constructor(stateConfig) {
    // 狀態描述配置
    this.stateConfig = stateConfig;
    // 當前狀態
    this.state = stateConfig.initial;
    // 上一個狀態
    this.lastState = "";
    // 狀態離開回調集合
    this.onExitMap = {};
    // 狀態進入回調集合
    this.onEntryMap = {};
    // 狀態改變回調
    this.handleStateChange = null;
  }

  /**
   * 改變狀態
   * @param type 行爲類型 描述當前狀態經過該類型的行爲轉變到另外一個狀態
   * @param arg 轉變過程當中的額外傳參
   * @returns {Promise<void>}
   */
  transition({ type, ...arg }) {
    const states = this.stateConfig.states;
    const curState = this.state;
    if (!states) {
      throw "states undefined";
    }
    if (!is("Object", states)) {
      throw "states should be object";
    }
    if (
      !states[curState] ||
      !states[curState]["on"] ||
      !states[curState]["on"][type]
    ) {
      console.warn(`transition fail, current state is ${this.state}`);
      return;
    }
    const nextState = states[curState]["on"][type];
    const curStateObj = states[curState];
    const nextStateObj = states[nextState];
    // 狀態轉變的經歷
    return (
      Promise.resolve()
        // 狀態離開
        .then(() =>
          this.handleLifeCycle({
            type: "onExit",
            stateObj: curStateObj,
            arg: { exitState: curState }
          })
        )
        // 狀態改變
        .then(() => this.updateState({ state: nextState, lastState: curState }))
        // 進入新狀態
        .then(() =>
          this.handleLifeCycle({
            type: "onEntry",
            stateObj: nextStateObj,
            arg: { state: nextState, lastState: curState, ...arg }
          })
        )
    );
  }

  /**
   * 狀態改變回調 只註冊一次
   * @param cb
   */
  onStateChange(cb) {
    cb &&
      is("Function", cb) &&
      !this.handleStateChange &&
      (this.handleStateChange = cb);
  }

  /**
   * 註冊狀態離開回調
   * @param type
   * @param cb
   */
  onExit(type, cb) {
    !this.onExitMap[type] && (this.onExitMap[type] = cb);
  }

  /**
   * 註冊狀態進入回調
   * @param type
   * @param cb
   */
  onEntry(type, cb) {
    !this.onEntryMap[type] && (this.onEntryMap[type] = cb);
  }

  /**
   * 更新狀態
   * @param state
   * @param lastState
   */
  updateState({ state, lastState }) {
    this.state = state;
    this.lastState = lastState;
    this.handleStateChange && this.handleStateChange({ state, lastState });
  }

  /**
   * 處理狀態轉變的生命週期
   * @param stateObj
   * @param type onExit/onEntry
   * @param arg
   * @returns {*}
   */
  handleLifeCycle({ stateObj, type, arg }) {
    const cbName = stateObj[type];
    if (cbName) {
      const cb = this[`${type}Map`][cbName];
      if (cb && is("Function", cb)) {
        return cb(arg);
      }
    }
  }

  /**
   * 獲取當前狀態
   * @returns {*}
   */
  getState() {
    return this.state;
  }

  /**
   * 獲取上一個狀態
   * @returns {string|*}
   */
  getLastState() {
    return this.lastState;
  }
}

而後這樣使用就好:

const stateConfig = {
  initial: "start",
  states: {
    start: {
      on: {
        INIT: "loadList"
      },
      onExit: "onExitStart"
    },
     loadList: {
      on: {
        LOAD_LIST_SUCCESS: "showList",
        LOAD_LIST_ERROR: "showListError"
      },
      onEntry: "onEntryLoadList"
    }
  }
}
/*
結果:
1.console.log('onExitStart')
2.console.log('onEntryLoadList')
3.console.log('transition success')
transition以及生命週期函數onExit、onEntry都支持promise控制異步流程
*/
const fsm = new Fsm(stateConfig);
transition({ type: "INIT"}).then(() => {
  console.log('transition success')
})
fsm.onExit('onExitStart', (data) => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('onExitStart')
      resolve()
    }, 1000)
  })
})
fsm.onEntry('onEntryLoadList', (data) => {
  console.log('onEntryLoadList')
})

總算把有限狀態機抽成一個工具來使用了,已經完成了最關鍵的一步。

集成到react去使用

若是想在react中使用,想到比較方便的使用形式是高階組件,須要用到有限狀態機的組件傳進高階組件,就立馬擁有了使用有限狀態機的能力。

import React from "react";
import { Fsm } from "../fsm";
export default function(stateConfig) {
  const fsm = new Fsm(stateConfig);
  return function(Component) {
    return class extends React.Component {
      constructor() {
        super();
        this.state = {
          machineState: {
            // 當前狀態
            value: stateConfig.initial,
            // 上一個狀態
            lastValue: ""
          }
        };
      }
      updateMachineState(data) {
        this.setState({
          machineState: { ...this.state.machineState, ...data }
        });
      }
      componentDidMount() {
        this.handleStateChange();
        this.handleEvent();
      }

      /**
       * 處理狀態更新
       */
      handleStateChange() {
        fsm.onStateChange(({ state, lastState }) => {
          this.updateMachineState({ value: state, lastValue: lastState });
        });
      }

      /**
       * 處理狀態改變事件
       */
      handleEvent() {
        const states = stateConfig.states;
        // 獲取狀態配置中全部的onEntry與onExit
        const eventObj = Object.keys(states).reduce(
          (obj, key) => {
            const value = states[key];
            const onEntry = value.onEntry;
            const onExit = value.onExit;
            onEntry && obj.onEntry.push(onEntry);
            onExit && obj.onExit.push(onExit);
            return obj;
          },
          {
            onEntry: [],
            onExit: []
          }
        );
        // 獲取組件實例中onEntry與onExit的回調方法
        Object.keys(eventObj).forEach(key => {
          eventObj[key].forEach(item => {
            this.ref[item] && fsm[key](item, this.ref[item].bind(this.ref));
          });
        });
      }
      render() {
        return (
          <Component
            ref={c => (this.ref = c)}
            {...this.state}
            transition={fsm.transition.bind(fsm)}
          />
        );
      }
    };
  };
}

使用的時候就能夠:

const stateConfig = {
  initial: "start",
  states: {
    start: {
      on: {
        INIT: "loadList"
      },
      onExit: "onExitStart"
    }
  }
}
class App extends Component {
    componentDidMount () {
        this.props.transition({ type: "INIT" });
    }
    onExitStart () {
        console.log('onExitStart ')
    }
}
export default withFsm(machine)(App);

如今咱們能夠愉快地使用這個高階組件將Todoist重構一遍
固然,大佬們會說了,個人項目比較複雜,有沒有比較完善的解決方案呢?那確定是有的,能夠看看react-automata,將xstate集成到react中使用。因爲咱們上面的小高階組件用法比較像react-automata,因此基本不須要什麼改動,就能夠遷移到react-automata,使用react-automata再重構一遍Todoist

最後

對於符合有限狀態機的使用場景,使用它確實能將狀態管理起來,由於咱們的狀態不再是那種如isSubmit = false/true那樣雜亂無章的狀態,而是某個時間節點裏的一個總括狀態。無論怎樣,有限狀態機的方案仍是促使了咱們去從新思考怎樣能更大程度地提升項目的可維護性,提供了一個新方向儘量減小祖傳代碼,改起bug或者需求的時候分析起來更加容易,終極目的只有一個,那就是,但願能早點下班。

相關文章
相關標籤/搜索