登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

XState父状态机怎样向子actor发送事件

来源:17golang原创

时间:2026-10-13 20:59:11 191浏览 收藏

在 XState 里,父状态机不能直接修改子 actor 的内部状态;正确做法是先让子 actor 处于运行状态,再用 sendTo 把事件发到它的 actor ID 或 actor ref。最容易出错的地方不是事件对象本身,而是把 machine 配置误当成 actor、ID 写错,或在 invoke 对应的状态已经退出后继续发送。

官方文档:https://stately.ai/docs/

要点速览
  • 固定 invoke 子 actor 时,父机用 sendTo('子actor的id', { type: '事件名' })。
  • 动态 spawn 的子 actor 优先使用 children 或 context 中保存的 actor ref。
  • 排查时依次确认目标存在、事件类型匹配、父状态没有停止或重建子 actor。

先确认父机手里拿的是正在运行的 actor

createMachine 返回的是 actor logic,createActor 返回的才是可以接收事件的 actor。父子通信发生在运行时:父机进入声明 invoke 的状态后,子 actor 才会按它的生命周期启动;父机离开该状态时,被 invoke 的子 actor 也会停止。

XState 父actor、invoke子actor、actor ID 与事件对象之间的静态关系说明图
图1:父 actor 通过稳定 ID 找到 invoke 子 actor,再发送事件的结构说明图,不是运行截图。

因此,下面的排查顺序很实用:先看父机当前是否仍在 invoke 状态,再看 snapshot.children 中是否有对应 ID,最后才检查事件名和负载。只要子 actor 已因状态退出而停止,继续发送就不会产生你期待的状态变化。

固定子 actor 用 sendTo 按 ID 发送

下面是一个最小的 XState v5 写法。父机收到外部的 START_WORK 后,把 RUN 事件发给 ID 为 worker 的被 invoke 子 actor。示例刻意把发送动作放在外部事件之后,避免把“父状态刚进入时子 actor 是否已经启动”混在第一个问题里。

import { createActor, createMachine, sendTo, setup } from 'xstate';

const workerMachine = createMachine({
  id: 'worker-machine',
  initial: 'idle',
  states: {
    idle: {
      on: {
        RUN: 'working', // 子 actor 只在 idle 状态接收 RUN
      },
    },
    working: {
      on: {
        STOP: 'idle', // 用显式事件回到可再次运行的状态
      },
    },
  },
});

const parentMachine = setup({
  actors: {
    worker: workerMachine,
  },
}).createMachine({
  id: 'parent-machine',
  initial: 'ready',
  states: {
    ready: {
      invoke: {
        id: 'worker', // sendTo 的目标 ID 必须与这里完全一致
        src: 'worker',
      },
      on: {
        START_WORK: {
          actions: sendTo('worker', { type: 'RUN' }), // 父 actor 向子 actor 发事件
        },
      },
    },
  },
});

const parentActor = createActor(parentMachine);
parentActor.start();
parentActor.send({ type: 'START_WORK' }); // 发送给父 actor,再由父机转发给 worker

这里有三个容易混淆的名字:src: 'worker' 是注册的 actor logic 名称,id: 'worker' 是运行中子 actor 的识别名,RUN 才是子机声明过的事件类型。父机发送的是事件对象,不是字符串;这也是 XState v5 与旧写法之间常见的迁移差异。

动态 spawn 时使用 actor ref

如果子 actor 数量不固定,例如每个上传任务对应一个 actor,就不应把所有目标都写成固定字符串。spawn 返回 actor ref,父机可以把它保存在上下文或直接通过 children 找到,再将事件发给这个具体引用。

import { createMachine } from 'xstate';

const uploadMachine = createMachine({
  id: 'upload',
  initial: 'queued',
  states: {
    queued: {
      on: {
        CANCEL: 'cancelled', // 动态子 actor 仍然通过自己的事件表改变状态
      },
    },
    cancelled: {},
  },
});

const uploadParent = createMachine({
  id: 'upload-parent',
  initial: 'active',
  states: {
    active: {
      entry: ({ spawn }) => {
        spawn(uploadMachine, { id: 'upload-42' }); // 实际项目可把 ref 放入 context
      },
      on: {
        CANCEL_UPLOAD: ({ children }, { sendTo }) => {
          const upload = children['upload-42'];
          if (upload) {
            sendTo(upload, { type: 'CANCEL' }); // 目标存在才发送,避免悬空引用
          }
        },
      },
    },
  },
});

动态场景的核心不是“把 ID 拼得更复杂”,而是持有 actor ref。实际代码还应在停止子 actor 的同一处移除 context 中保存的 ref,否则父机可能继续握着一个已经停止的对象。对于多个上传任务,可以用任务 ID 建立自己的映射,但映射里的 ref 必须和当前存活的子 actor 同步。

XState 固定 ID 与动态 actor ref 的发送目标、事件契约和生命周期检查关系图
图2:固定 ID、动态 actor ref 与事件契约之间的排查结构图,不是调试器截图。

事件没有到达时按三层检查

第一层是目标。先确认 sendTo 指向的 ID 正是 invoke 配置中的 id,或者 actor ref 仍存在。不要把 src 名称和 id 当成同一个概念,也不要在父机已经离开 invoke 状态后继续用旧目标。

第二层是事件契约。子机必须在当前状态的 on 中声明相同的 type。例如子机只处理 RUN,父机发送 START,事件可以到达但不会触发目标转移。带数据时再检查负载字段是否符合 TypeScript 类型。

第三层是生命周期。invoke 子 actor 随声明它的状态进入和退出;spawn 子 actor 则由父 actor 持有,通常要显式停止。父机重进状态时,旧 invoke 子 actor 可能已经被停止并换成新的实例,保存在 context 里的旧 ref 不应继续使用。

现象优先检查修复方向
动作执行但子机无变化事件 type 是否写成子机当前 on 的名称统一事件对象和负载类型
发送到固定字符串失败invoke 的 id 是否与 sendTo 目标一致稳定 ID,别误用 src 名称
动态任务偶尔收不到事件actor ref 是否已停止或被替换同步 children/context 映射

常见问题

父机能不能直接改子 actor 的 context?

不应该。actor 的内部状态由它自己根据收到的事件更新;父机应发送语义明确的事件,让子机决定如何改变 context。

固定 invoke 和动态 spawn 都能用 sendTo 吗?

可以,但固定 invoke 通常按稳定 ID 发送,动态 spawn 更适合使用 actor ref。目标选择应跟随生命周期模型,而不是只看代码短不短。

子 actor 怎样把结果通知父 actor?

可以通过父 actor ref 使用 sendTo 回传事件;旧项目也可能看到 sendParent,但显式传递 actor ref 更容易表达依赖并保持类型边界。

把父子通信拆成“目标、事件、生命周期”三层后,XState 的事件转发就不再是黑盒:父机负责找到正确的 actor,子机负责解释事件,状态机负责决定这个 actor 是否仍然存活。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>