引言

我们平时开发时本质上就时对应用程序的各种状态进行切换并作出相应处理,最直接的方法就是添加标志位然后考虑所有可能出现的边界问题,通过if...else if...else 来对当前状态进行判断从而达成页面的交互效果, 但随着业务需求的增加各种状态也会随之增多,我们就不得不再次修改if...else代码或者增加对应的判断,最终使得程序的可读性、扩展性、维护性变得很麻烦

有限状态机,(英语:Finite-state machine, FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。

利用有限状态机我们可以将条件判断的结果转化为状态对象内部的状态,并且能够使用对应的方法,进行对应的改变。这样方便了对状态的管理也会很容易,也是更好的实践了UI=fn(state)思想。

举个栗子🌰

我们这里用一个简易的红绿灯案例,实现一个简易的有限状态机,并且可以通过每一个状态暴露出来的方法,改变当前的状态

const door = machine({
  RED: {
    yello: "YELLO",
  },
  GREEN: {
    red: "RED",
  },
  YELLO: {
    green: "GREEN",
  },
});
  • 首先初始时door的状态显示为红灯即RED
  • 当我们进行yello操作的时候,状态变成黄灯,即状态改变为YELLO
  • 当我们进行green操作的时候,状态变成绿灯,即状态改变为GREEN
  • 当我们连着进行red操作、yello操作的时候,最终状态变成黄灯,即状态改变为YELLO ...

从零开始

通过接受一个对象(如果是函数就执行),拿到初始值,并且在函数内部维护一个变量记录当前的状态,并且记录第一个状态为初始状态

const machine = statesObject => {
  if (typeof statesObject == "function") statesObject = statesObject();
  let currentState;
  for (const stateName in statesObject) {
    currentState = currentState || statesObject[stateName];
  }
};

获取状态

因为当前状态是通过函数局部变量currentState进行保存,我们需要一些方法

  • getMachineState:获取当前的状态
  • getMachineEvents:获取当前状态上保存了哪些方法

这两个函数通过stateMachine进行保存并作为函数结果进行返回

const machine = statesObject => {
  let currentState
  ...
  const getMachineState = () => currentState.name;
  const getMachineEvents = () => {
    const events = [];
    for (const property in currentState) {
      if (typeof currentState[property] == "function") events.push(property);
    }
    return events;
  };
  const stateMachine = { getMachineState, getMachineEvents };
  ...
  return stateMachine
};

状态改变

我们进行改变的时候,调用的是一开始配置好的方法对状态进行更改,此时需要将每一个状态合并到stateStore中进行保存

再将对应的方法作为偏函数(函数预先将转换的状态和方法进行传递),保存在stateMachinestateMachine会作为结果进行返回),这样就可以

  • 使用.yello().red().green()的方法,改变状态
  • 使用.getMachineState().getMachineEvents()查看当前状态和查看当前状态对应的方法
const machine = statesObject => {
  if (typeof statesObject == "function") statesObject = statesObject();
  let currentState;
  const stateStore = {};
  const getMachineState = () => currentState.name;
  const getMachineEvents = () => {
    const events = [];
    for (const property in currentState) {
      if (typeof currentState[property] == "function") events.push(property);
    }
    return events;
  };
  const stateMachine = { getMachineState, getMachineEvents };
  for (const stateName in statesObject) {
    stateStore[stateName] = statesObject[stateName];
    for (const event in stateStore[stateName]) {
      stateMachine[event] = transition.bind(null, stateName, event);
    }
    stateStore[stateName].name = stateName;
    currentState = currentState || stateStore[stateName];
  }
  return stateMachine;
};

transition

上面代码中最重要的莫过于transition函数,即改变当前状态,在stateStore中获取当前的要更改的状态名,重新给currentState赋值,并返回stateMachine供函数继续链式调用

const machine = statesObject => {
  ...
  const transition = (stateName, eventName) => {
    currentState = stateStore[stateName][eventName];
    return stateMachine;
  };
  for (const stateName in statesObject) {
    stateStore[stateName] = statesObject[stateName];
    for (const event in stateStore[stateName]) {
      stateMachine[event] = transition.bind(null, stateName, event);
    }
    stateStore[stateName].name = stateName;
    currentState = currentState || stateStore[stateName];
  }
  return stateMachine;
};

看似没有问题,但是如果我们按照上面的代码执行后,获得的状态值为undefined,因为我们在getMachineState时,获取到的是currentState.name,而不是currentState,所以此时在获取状态的时候需要用通过函数进行获取obj => obj[xxx]

const machine = statesObject => {
  ...
  const transition = (stateName, eventName) => {
    currentState = stateStore[stateName][eventName](stateStore);
    return stateMachine;
  };
  for (const stateName in statesObject) {
    stateStore[stateName] = statesObject[stateName];
    for (const event in stateStore[stateName]) {
      const item = stateStore[stateName][event];
      if (typeof item == "string") {
        stateStore[stateName][event] = obj => obj[item];
        stateMachine[event] = transition.bind(null, stateName, event);
      }
    }
    stateStore[stateName].name = stateName;
    currentState = currentState || stateStore[stateName];
  }
  return stateMachine;
};

实现fsm状态机

现在我们实现了一个完整的fsm,当我们配置好状态机时

const door = machine({
  RED: {
    yello: "YELLO",
  },
  GREEN: {
    red: "RED",
  },
  YELLO: {
    green: "GREEN",
  },
});

执行如下操作时,会打印我们想要的结果

  • door.getMachineState() --> RED
  • door.yello().getMachineState() --> YELLO
  • door.green().getMachineState() --> GREEN
  • door.red().yello().getMachineState() --> YELLO

实现钩子函数

但是我们监听不到状态机的改变,所以当我们想监听状态变换时,应该从内部暴露出钩子函数,这样可以监听到状态机内部的变化,又能进行一些副作用操作

对此,可以对transition进行一些改造,将对于currentState状态的改变用方法setMachineState去处理

setMachineState函数会拦截当前状态上绑定onChange方法进行触发,并将改变状态的函数改变前的状态改变后的状态传递出去

const machine = statesObject => {
  ...
  const setMachineState = (nextState, eventName) => {
    let onChangeState;
    let lastState = currentState;
    const resolveSpecialEventFn = (stateName, fnName) => {
      for (let property in stateStore[stateName]) {
        if (property.toLowerCase() === fnName.toLowerCase()) {
          return stateStore[stateName][property];
        }
      }
    };
    currentState = nextState;
    onChangeState = resolveSpecialEventFn(lastState.name, "onChange");
    if (
      onChangeState &&
      typeof onChangeState == "function" &&
      lastState.name != currentState.name
    ) {
      onChangeState.call(
        stateStore,
        eventName,
        lastState.name,
        currentState.name
      );
    }
  };
  const transition = (stateName, eventName) => {
    const curState = stateStore[stateName][eventName](stateStore);
    setMachineState(curState, eventName);
    return stateMachine;
  };
  ...
  return stateMachine;
};

这样我们在调用时,状态的每一次改变都可以监听到,并且可以执行对应的副作用函数

const door = machine({
  RED: {
    yello: "YELLO",
    onChange(fn, from, to) {
      console.log(fn, from, to, "onChange");
    },
  },
  GREEN: {
    red: "RED",
    onChange(fn, from, to) {
      console.log(fn, from, to, "onChange");
    },
  },
  YELLO: {
    green: "GREEN",
    onChange(fn, from, to) {
      console.log(fn, from, to, "onChange");
    },
  },
});

完整代码

export default statesObject => {
  if (typeof statesObject == "function") statesObject = statesObject();
  let currentState;
  const stateStore = {};
  const getMachineState = () => currentState.name;
  const getMachineEvents = () => {
    let events = [];
    for (const property in currentState) {
      if (typeof currentState[property] == "function") events.push(property);
    }
    return events;
  };
  const stateMachine = { getMachineState, getMachineEvents };
  const setMachineState = (nextState, eventName) => {
    let onChangeState;
    let lastState = currentState;
    const resolveSpecialEventFn = (stateName, fnName) => {
      for (let property in stateStore[stateName]) {
        if (property.toLowerCase() === fnName.toLowerCase()) {
          return stateStore[stateName][property];
        }
      }
    };
    currentState = nextState;
    onChangeState = resolveSpecialEventFn(lastState.name, "onChange");
    if (
      onChangeState &&
      typeof onChangeState == "function" &&
      lastState.name != currentState.name
    ) {
      onChangeState.call(
        stateStore,
        eventName,
        lastState.name,
        currentState.name
      );
    }
  };
  const transition = (stateName, eventName) => {
    const curState = stateStore[stateName][eventName](stateStore);
    setMachineState(curState, eventName);
    return stateMachine;
  };
  for (const stateName in statesObject) {
    stateStore[stateName] = statesObject[stateName];
    for (const event in stateStore[stateName]) {
      const item = stateStore[stateName][event];
      if (typeof item == "string") {
        stateStore[stateName][event] = obj => obj[item];
        stateMachine[event] = transition.bind(null, stateName, event);
      }
    }
    stateStore[stateName].name = stateName;
    currentState = currentState || stateStore[stateName];
  }
  return stateMachine;
};

总结

这个 fsm有限状态机 主要完成了:

  • 状态的可观测
  • 状态的链式调用
  • 状态变化的钩子函数

项目代码:github.com/blazer233/a…

参考轮子:github.com/fschaefer/S…

以上就是JS前端实现fsm有限状态机实例详解的详细内容,更多关于JS前端fsm有限状态机的资料请关注阿兔在线工具其它相关文章!

点赞(0)

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部