导读:本期聚焦于书生创作的《如何用现代JavaScript实现一个状态机(State Machine)?》,敬请观看详情。状态机听起来像是编译原理课程里的概念,但在前端开发中它几乎无处不在:一个下载按钮要经历空闲、加载中、成功、失败四种状态,一个视频播放器要在播放、暂停、缓冲之间来回切换。用一堆布尔值去管理这些状态,很快就会写出if嵌套地狱,还可能出现同时处于两个状态的bug。本文从有限状态机的核心概念讲起,先用原生JavaScript手写一个轻量级状态机,支持状态定义、事件触发和过渡校验,再对比XState这类成熟库的使用方式,最后结合订单流转的真实场景,分析状态机在复杂交互和状态管理中的落地技巧与常见坑点。

状态机(State Machine)这个词听起来很学术,但它描述的问题非常接地气:一个对象在任意时刻只能处于有限个状态中的一个,状态的改变只能通过明确的事件触发,并且每次改变都遵循预先定义好的规则。比如一个文件上传组件,它可以是“待选择”“上传中”“暂停”“成功”“失败”中的某一个状态,绝不可能既是“上传中”又是“成功”。很多前端代码出问题的根源,就在于用一堆isLoadingisSuccessisPaused布尔值去表达本该互斥的状态,组合爆炸之后逻辑根本理不清。这篇文章带你用现代JavaScript从零实现一个实用的状态机,并看看它和主流库之间的关系。

如何用现代JavaScript实现一个状态机(State Machine)?

一、为什么布尔值管理状态注定会失控

先看一段典型的“布尔值地狱”代码。假设我们要做一个登录表单,有未提交、提交中、成功、失败四种状态,常见写法是这样的:

const state = {
  isIdle: true,
  isSubmitting: false,
  isSuccess: false,
  isError: false
};

function handleSubmit() {
  if (state.isIdle && !state.isSubmitting) {
    state.isSubmitting = true;
    state.isIdle = false;
    // 发请求...
  } else if (state.isSuccess) {
    // 已经成功了,还能再提交吗?
  }
}

这段代码的问题非常明显:四个布尔变量理论上有16种组合,但其中12种组合是非法的(比如isSuccessisError同时为true)。没有任何机制阻止你写出非法组合,只能靠开发者在每个赋值点小心翼翼地维护所有标志位。一旦状态增加到七八个,组合数量会瞬间失控。

状态机的思路恰恰相反:把状态本身压缩成一个单一字符串,把“允许的变化路径”显式声明出来。判断状态时只需要state.current === 'submitting',切换状态时框架自动校验“当前状态加上这个事件,是否有一条合法的过渡路径”。非法操作在定义阶段就被拦截,而不是等到运行时出现诡异bug再回头排查。

二、用原生JavaScript手写一个轻量级状态机

实现一个够用的状态机只需要几十行代码。核心思路是三个部分:状态定义表、当前状态指针、事件分发方法。我们用ES6的类语法和Proxy来实现,顺便支持过渡前后的钩子函数。

class StateMachine {
  constructor({ initial, states }) {
    this.current = initial;
    this.states = states;       // 状态定义表
    this.listeners = new Set(); // 订阅者,用于响应状态变化
  }

  // 派发事件,尝试从当前状态过渡到下一个状态
  dispatch(event, payload) {
    const stateDef = this.states[this.current];
    if (!stateDef) {
      throw new Error(`未定义的状态: ${this.current}`);
    }
    const transition = stateDef.on?.[event];
    if (!transition) {
      throw new Error(
        `状态 "${this.current}" 不允许触发事件 "${event}"`
      );
    }
    // 过渡前钩子,返回 false 可以取消这次过渡
    if (stateDef.before && stateDef.before(event, payload) === false) {
      return false;
    }
    const prev = this.current;
    this.current = typeof transition === 'string'
      ? transition
      : transition.target;
    this.listeners.forEach(fn => fn(this.current, prev, event, payload));
    return true;
  }

  // 判断当前是否可以处理某个事件
  can(event) {
    return Boolean(this.states[this.current]?.on?.[event]);
  }

  // 订阅状态变化
  subscribe(fn) {
    this.listeners.add(fn);
    return () => this.listeners.delete(fn);
  }
}

用这个类来描述一个下载按钮的生命周期,代码会变得非常直观。每个状态声明它能响应哪些事件、响应后跳到哪里,非法流程直接在dispatch时抛错:

const machine = new StateMachine({
  initial: 'idle',
  states: {
    idle: {
      on: { CLICK: 'loading' }
    },
    loading: {
      on: {
        SUCCESS: 'success',
        ERROR: 'error',
        CANCEL: 'idle'
      }
    },
    success: { on: { RESET: 'idle' } },
    error:   { on: { RETRY: 'loading', RESET: 'idle' } }
  }
});

machine.subscribe((curr, prev) => {
  console.log(`${prev} -> ${curr}`);
});

machine.dispatch('CLICK');   // idle -> loading
machine.dispatch('SUCCESS'); // loading -> success
machine.dispatch('CLICK');   // 抛错:success 状态不允许 CLICK

这个实现的优点是零依赖、代码量小、行为完全可控,适合嵌入到任何框架里。它也天然契合React的useReducer模式——把dispatch包进reducer,组件就能根据machine.current渲染对应UI,状态迁移逻辑和视图渲染完全解耦。

三、复杂场景:交给XState还是继续手写?

当状态机开始出现嵌套状态、并行状态、延时自动过渡这些需求时,手写实现的复杂度会陡增。比如订单系统里“已支付”状态内部还有“待发货、已发货、已签收”子状态,或者需要“支付后30分钟未确认则自动取消”这种定时逻辑。这时候XState这样的成熟库就值得引入了。

import { setup } from 'xstate';

const orderMachine = setup({
  actions: {
    notifyUser: () => { /* 发送通知 */ }
  }
}).createMachine({
  id: 'order',
  initial: 'created',
  states: {
    created: {
      after: {
        1800000: { target: 'cancelled' } // 30分钟未支付自动取消
      },
      on: { PAY: 'paid' }
    },
    paid: {
      initial: 'pending',
      states: {
        pending: { on: { SHIP: 'shipped' } },
        shipped: { on: { SIGN: 'signed' } },
        signed: { type: 'final' }
      }
    },
    cancelled: {
      entry: 'notifyUser',
      type: 'final'
    }
  }
});

XState提供了可视化工具,可以把机器定义直接渲染成状态图,对排查复杂流程特别有用。它的after语法天然支持延时过渡,嵌套状态的写法也比自己维护父子关系清爽得多。

不过引入XState也有代价:包体积、学习曲线、团队熟悉度都需要评估。一个实用的判断标准是——如果状态数量不超过五个、没有嵌套和并行需求、流程基本是线性的,手写的几十行类完全够用;一旦出现层级结构或需要时间驱动的过渡,再考虑上库也不迟。千万不要为了炫技在两三态的简单场景里堆重型依赖,那是对状态机思想的误解。

四、落地时的几个常见坑

第一个坑是事件命名。状态机里事件应该描述“发生了什么”而不是“要去哪”,比如用SUCCESS而不是GO_TO_SUCCESS。因为同一个事件在不同状态下可能导向不同的目标状态,事件名绑定了语义,过渡表才灵活。

第二个坑是副作用的位置。不要在状态切换的dispatch调用里顺手发请求、弹提示,副作用应该放在过渡钩子(entry、exit)或订阅回调中。这样状态机本身保持纯粹的描述逻辑,测试时不需要mock任何网络请求,只验证过渡表是否正确即可。

第三个坑是忘记处理“不允许的事件”。手写实现里抛异常是最直接的方式,但在UI层面更好的做法是先调用can(event)判断,据此禁用按钮或忽略点击。让非法操作根本无法被用户触发,比触发后报错体验好得多。

总结一下,状态机的价值不在于那个类怎么写,而在于它强制你提前把所有状态和合法路径想清楚。这份“想清楚”的产物就是过渡表,它既是代码,也是文档,也是测试用例的来源。下次再看到组件里五六个布尔标志纠缠不清时,不妨停下来画一张状态图,往往问题在画图阶段就解决了一半。

JavaScript状态机状态机实现状态管理修改时间:2026-09-13 16:12:59

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260913/56110.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。