State Management

XState: акторы

Чат-виджет Intercom держит на одной странице десятки независимых процессов одновременно: сокет переподключается, набор сообщения тротлится, загрузка вложения тикает прогрессом, фоновый опрос тянет непрочитанные. Если описать всё это одной плоской машиной, она распухает до сотни состояний и читать её невозможно. XState v5 переворачивает подход: вместо одной гигантской машины - набор мелких акторов, каждый со своим состоянием, которые шлют друг другу события. Машина сокета не знает про индикатор набора, и это правильно.

  • Stately.ai (авторы XState) строят редактор машин на акторах: каждая панель и каждое соединение это отдельный спавненный актор
  • Чат-виджеты вроде Intercom и Crisp: сокет, тайпинг-индикатор, очередь исходящих сообщений - независимые акторы под одним родителем
  • Онбординг-визарды в SaaS: каждый шаг это инвокнутая машина, родитель оркеструет переходы и собирает результат
  • Игровые лобби и редакторы (Figma-подобные): десятки живых сущностей, каждая со своим жизненным циклом, общаются сообщениями
  • Загрузчики файлов: на каждый файл спавнится актор с прогрессом, ретраями и отменой, не мешая соседям

Предварительные знания

  • Машина состояний: состояния, переходы, события и контекст
  • Базовый XState v5: createMachine, createActor, setup
  • Промисы и async/await на уровне уверенного использования
  • XState: основы

Как XState пришёл к акторам

Дэвид Куршид выпустил XState в 2017 как реализацию SCXML-машин состояний для JavaScript. Несколько лет основной единицей была одна машина с вложенными (иерархическими и параллельными) состояниями. Но реальные приложения это не одна машина, а множество живых процессов, общающихся между собой - ровно то, что Карл Хьюитт описал актор-моделью ещё в 1973 году. XState v5 (релиз в конце 2023) сделал актор-модель фундаментом: теперь любой запущенный объект это актор, машина это лишь один из видов акторной логики, а invoke и spawn создают дочерних акторов. Каждый актор имеет адрес, почтовый ящик событий и собственное изолированное состояние.

Что такое актор и почему v5 на них построен

Актор это независимая единица вычисления со своим изолированным состоянием и почтовым ящиком (mailbox) входящих событий. Актор не открывает своё состояние наружу и не трогает чужое напрямую. Единственный способ взаимодействия - отправить сообщение. Получив событие, актор может изменить своё состояние, отправить сообщения другим акторам и создать новых акторов. Эту модель сформулировал Карл Хьюитт в 1973 году, и она же лежит в основе Erlang и Akka.

В XState v5 актор - базовая абстракция. Машина состояний это один из видов акторной логики, но не единственный: актором может быть и обёрнутый промис, и observable, и колбэк-логика. createActor запускает корневого актора, а invoke и spawn порождают дочерних. У каждого актора есть адрес (ссылка), через который ему шлют события, и снимок состояния (snapshot), который можно читать, но не мутировать.

  • Прямой вызов (общий стейт) — Модули дёргают методы друг друга и читают чужие поля. Связность высокая, гонки появляются там, где два места правят одно состояние.
  • Актор-модель (сообщения) — Состояние спрятано внутри актора, наружу только события. Связность низкая, каждый актор обрабатывает свой почтовый ящик последовательно.

Изоляция состояния это и есть ключевое свойство. Раз внешний код не может изменить состояние актора напрямую, единственная точка изменения - обработка события внутри. Это убирает целый класс багов с гонками за общую переменную.

Что отличает актора от обычного объекта с публичными методами?

invoke против spawn

Дочерних акторов в XState создают двумя способами, и разница в их жизненном цикле. invoke привязывает актора к конкретному состоянию: пока состояние активно, актор жив, при выходе из состояния он автоматически останавливается. Это удобно, когда дочерний процесс заранее известен и логически принадлежит одному состоянию - например, запрос к серверу во время состояния loading.

spawn создаёт актора динамически прямо в действии, и такой актор не привязан к одному состоянию - он живёт, пока его явно не остановить или пока родитель жив. spawn нужен, когда число дочерних акторов заранее неизвестно: по одному на каждый загружаемый файл, на каждое сообщение в очереди, на каждое соединение. Ссылки на спавненных акторов обычно складывают в контекст родителя.

Свойствоinvokespawn
Где объявляетсяВ конфигурации состоянияВ действии (assign/raise)
Жизненный циклПока активно состояниеПока не остановлен явно или жив родитель
Число акторовФиксированное, известно заранееДинамическое, неизвестно заранее
Типичный кейсЗапрос на время loading, дочерняя машина шагаПо актору на файл, сообщение, соединение

Простое правило выбора: если дочерний процесс жёстко привязан к состоянию и его ровно один - invoke. Если процессов несколько и их число меняется в рантайме - spawn со ссылками в контексте.

Менеджер загрузок позволяет добавлять произвольное число файлов, и на каждый нужен свой прогресс и отмена. Что подходит?

Как акторы общаются событиями

Акторы не читают и не пишут состояние друг друга напрямую - они обмениваются событиями. Родитель шлёт сообщение конкретному ребёнку через sendTo, указывая его ссылку или id. Ребёнок отвечает родителю через sendParent. Если родитель инвокнул промис-актора, результат приходит как событие onDone, а ошибка как onError. Так строится дерево акторов, где общение идёт по адресам, а не по общим переменным.

Каждый актор обрабатывает свой почтовый ящик последовательно: события встают в очередь и применяются по одному. Поэтому внутри одного актора гонок за его собственное состояние не бывает. Параллелизм возникает между акторами, и координируется он сообщениями, а не блокировками. Это та же дисциплина, что в Erlang: тысячи процессов, каждый со своим состоянием, общение только сообщениями.

Соблазн обойти модель: дотянуться до снимка дочернего актора и прочитать его поля в обход событий. Читать snapshot допустимо, но менять состояние ребёнка можно только событием через sendTo. Прямая мутация чужого контекста ломает изоляцию и возвращает гонки.

Дочерний актор завершил работу и должен передать результат родителю. Как это делается в актор-модели XState?

Связь с другими темами

Урок про акторов как способ декомпозиции. Дальше тема раскрывается так:

  • XState: основы — Машина из базового урока становится логикой одного актора. Акторы это следующий слой над ней
  • Машины: когда уместны — Акторы мощны, но не бесплатны. Следующий урок про границу, за которой машина оправдана

Итог

  • Актор-модель: независимая единица со своим изолированным состоянием и почтовым ящиком событий, общается только сообщениями. XState v5 построен на ней целиком
  • invoke привязывает дочернего актора к состоянию: актор живёт, пока активно состояние, и автоматически останавливается при выходе из него
  • spawn создаёт актора динамически из действия, когда число дочерних сущностей заранее неизвестно (по актору на файл, на сообщение, на соединение)
  • Акторы не лезут в чужое состояние напрямую: родитель шлёт событие через sendTo, ребёнок отвечает через sendParent или sendBack
  • Декомпозиция на акторов держит каждую машину маленькой и читаемой вместо одной плоской машины на сотню состояний

Связанные уроки

  • sm-26-xstate-basics — Акторы наслаиваются на машину состояний из базового урока: без понимания состояний и переходов разговор про spawn не складывается
  • sm-29-xstate-when — Поняв акторов, легче решать, где машина оправдана, а где это перебор
XState: акторы

0

1

Войти