简单记录下 状态机

24 阅读5分钟

以下是简单的状态机设计模式和简单示例,核心是先理解状态机的核心思想,再通过极简示例掌握用法,最后扩展到通用设计模式。

一、先搞懂:状态机的核心思想(人话版)

状态机本质是「把复杂的状态切换规则写死,让程序只能按规则走」,就像:

  • 玩游戏时,角色「站立」只能切「行走/跳跃」,不能直接切「死亡」;
  • 点外卖时,「待付款」只能切「付款中/取消」,不能直接切「已收货」。 核心三要素(记住这3个,状态机就懂了):
  1. 状态(State): 程序当前的“身份”(比如:空闲、加载中、成功、失败);
  2. 事件(Event):触发状态变化的“动作”(比如:点击“加载”、请求成功、请求失败);
  3. 规则(Transition):“什么状态下触发什么事件,能切到什么新状态”(比如:空闲→点击加载→加载中)。

二、前端状态机的通用设计模式(极简版)

不用依赖任何库,纯原生JS就能实现,核心结构就4部分:

// 状态机设计模式模板(通用) 
const StateMachine = { 
    // 1. 初始状态 
    currentState: "初始状态", 
    // 2. 状态切换规则:{ 当前状态: { 事件: 目标状态 } } 
    rules: {}, 
    // 3. 触发状态切换的核心方法
    trigger(event, payload) { 
        // 第一步:查规则,判断当前状态能不能触发这个事件 
        const nextState = this.rules[this.currentState]?.[event]; 
        if (!nextState) { 
            console.warn(`❌ ${this.currentState}状态下不能触发${event}事件`); 
            return; 
        } 
        // 第二步:记录旧状态,切换到新状态 
        const oldState = this.currentState; 
        this.currentState = nextState; 
        // 第三步:执行状态切换后的逻辑(比如更新UI、调接口) 
        this.onChange(nextState, oldState, payload); 
    }, 
    // 4. 状态变更后的回调(自定义业务逻辑) 
    onChange(newState, oldState, payload) { 
        console.log(`✅ 状态变更:${oldState}${newState}`, payload); 
    }, 
};


三、最简单示例:

按钮点击的加载状态机 用「按钮点击请求数据」这个前端最常见的场景,实现状态机,一看就懂:

需求

按钮有4个状态:

  • 初始:idle(空闲,可点击);
  • 点击后:loading(加载中,禁用按钮);
  • 请求成功:success(显示成功,3秒后重置为空闲);
  • 请求失败:error(显示失败,可重新点击)。

完整代码(可直接复制运行)

<!DOCTYPE html> 
<html> 
<body> 
    <!-- UI:一个按钮 + 状态提示 --> 
    <button id="btn">点击加载数据</button> 
    <div id="status">当前状态:空闲</div> 
    <script> 
        // 1. 初始化状态机 
        const loadDataFSM = { 
            // 初始状态:空闲 
            currentState: "idle", 
            // 2. 定义状态切换规则(核心!) 
            rules: { idle: { click: "loading" },
            // 空闲→点击→加载中 
            loading: { success: "success", fail: "error" }, 
            // 加载中→成功/失败 
            success: { reset: "idle" }, 
            // 成功→重置→空闲 
            error: { retry: "loading" } 
            // 失败→重试→加载中 
        }, 
        // 3. 触发状态切换的方法 
        trigger(event, payload) { 
            const nextState = this.rules[this.currentState]?.[event]; 
            if (!nextState) { 
                alert(`当前${this.currentState}状态,不能${event}!`); 
                return; 
            } 
            const oldState = this.currentState; 
            this.currentState = nextState; 
            this.onChange(newState, oldState, payload); 
        }, 
        // 4. 状态变更后更新UI(业务逻辑) 
        onChange(newState, oldState, payload) { 
            const btn = document.getElementById("btn");
            const status = document.getElementById("status"); 
            // 根据新状态更新UI 
            switch (newState) { 
                case "loading": 
                    btn.disabled = true; 
                    btn.innerText = "加载中..."; 
                    status.innerText = "当前状态:加载中"; 
                    // 模拟异步请求 
                    mockRequest().then(() => { 
                        this.trigger("success");  // 请求成功→切换到success 
                    }).catch(() => { 
                        this.trigger("fail"); // 请求失败→切换到error 
                    }); 
                    break; 
                case "success": 
                    btn.disabled = false; 
                    btn.innerText = "点击加载数据"; 
                    status.innerText = "当前状态:成功(3秒后重置)"; // 3秒后重置为空闲
                    setTimeout(() => this.trigger("reset"), 3000); 
                    break; 
                case "error": 
                    btn.disabled = false; 
                    btn.innerText = "重新加载"; 
                    status.innerText = "当前状态:失败,可重试"; 
                    break; 
                case "idle": 
                    btn.innerText = "点击加载数据";
                    status.innerText = "当前状态:空闲"; 
                    break; 
                } 
            } 
        }; 
        // 模拟异步请求(50%成功率) 
        function mockRequest() { 
            return new Promise((resolve, reject) => { 
                setTimeout(() => { 
                    Math.random() > 0.5 ? resolve() : reject(); 
                }, 1000); 
        }); 
        } 
        // 绑定按钮点击事件 
        document.getElementById("btn").addEventListener("click", () => { 
            // 点击按钮:触发对应事件(idle态触发click,error态触发retry) 
            const event = loadDataFSM.currentState === "idle" ? "click" : "retry";
            loadDataFSM.trigger(event); 
        }); 
    </script> 
</body> 
</html> 

运行效果

  1. 初始:按钮可点击,显示“点击加载数据”,状态提示“空闲”;
  2. 点击按钮:按钮禁用,显示“加载中...”,1秒后随机切换:
    • 成功:按钮恢复,显示“点击加载数据”,3秒后重置为初始态;
    • 失败:按钮恢复,显示“重新加载”,点击可重试;
  3. 非法操作拦截:比如“加载中”时重复点击按钮,会提示“加载中状态下不能click事件”。

四、核心设计思路拆解(通俗易懂)

1. 为什么这么设计?

  • 把「状态规则」和「业务逻辑」分开:规则写在rules里,UI逻辑写在onChange里,不用混在一起;
  • 所有状态切换都走trigger方法,相当于“唯一入口”,避免直接改currentState导致的混乱;
  • 规则是“显性的”,比如看rules就知道:
    rules: { 
        idle: { click: "loading" }, 
        loading: { success: "success", fail: "error" }, 
        success: { reset: "idle" }, 
        error: { retry: "loading" }
    }

不用逐行读代码,就能知道“什么状态能做什么事”。

2. 关键细节(新手必看)

  • trigger方法是“门卫”:先查规则,再切换状态,最后执行逻辑,一步都不能少;
  • onChange方法是“执行者”:只负责状态变更后的业务(比如更UI、调接口),不关心怎么切换的;
  • 永远不要直接改currentState:比如不要写loadDataFSM.currentState = "success",必须通过trigger触发,否则会绕过规则校验。

五、扩展:更通用的状态机类(可复用)

如果多个场景要用状态机,可封装成类,复用性更高:

// 通用状态机类 
class SimpleFSM { 
    constructor(initialState, rules, onChange) { 
        this.currentState = initialState; // 初始状态 
        this.rules = rules; // 切换规则 
        this.onChange = onChange; // 状态变更回调 
    } 
    // 触发状态切换 
    trigger(event, payload) { 
        const nextState = this.rules[this.currentState]?.[event]; 
        if (!nextState) { 
            console.warn(`❌ 非法操作:${this.currentState}${event}`); 
            return false; 
        } 
        const oldState = this.currentState; 
        this.currentState = nextState; 
        this.onChange?.(nextState, oldState, payload); // 执行回调 
        return true; 
    } 
    // 获取当前状态 
    getState() { 
        return this.currentState; 
    } 
}
// 用法:创建一个弹窗状态机 
const modalFSM = new SimpleFSM( 
   "closed", // 初始状态:关闭 
   // 切换规则 
   { 
       closed: { open: "opened" }, 
       opened: { close: "closed", confirm: "closed" } 
   }, 
   // 状态变更回调(更新弹窗UI) 
   (newState) => { 
       const modal = document.getElementById("modal"); 
       modal.style.display = newState === "opened" ? "block" : "none"; 
   } 
   );
  // 调用示例 
  modalFSM.trigger("open"); // 打开弹窗 
  modalFSM.trigger("confirm");// 确认关闭弹窗 
  modalFSM.trigger("open"); // 非法操作:closed态才能open,opened态不行,会提示

总结(关键点回顾)

  1. 状态机核心三要素:状态(当前身份)、事件(触发动作)、规则(切换逻辑);
  2. 设计模式核心:一个唯一的切换入口(trigger)+ 显性的规则(rules)+ 分离的业务逻辑(onChange);
  3. 核心好处:拦截非法操作、避免状态混乱、逻辑清晰易维护,哪怕是简单场景也能少写bug。 这个设计模式不用依赖任何第三方库,纯原生JS就能实现,新手也能快速上手,是前端状态机最基础、最易懂的写法。