Отложенные события и переходы¶
Задержки и таймауты можно декларативно обрабатывать с помощью диаграмм состояний.
Отложенные переходы¶
Переходы могут выполняться автоматически после задержки. Это настраивается в состоянии в свойстве after
, которое задает миллисекундные задержки на переходы:
const lightDelayMachine = createMachine({
id: 'lightDelay',
initial: 'green',
states: {
green: {
after: {
// after 1 second, transition to yellow
1000: { target: 'yellow' },
},
},
yellow: {
after: {
// after 0.5 seconds, transition to red
500: { target: 'red' },
},
},
red: {
after: {
// after 2 seconds, transition to green
2000: { target: 'green' },
},
},
},
});
Отложенные переходы можно указать так же, как вы указываете их в свойстве on: ...
Они могут быть явными:
// ...
states: {
green: {
after: {
1000: { target: 'yellow' }
}
}
}
// ...
Отложенные переходы также могут быть условными по отношению к единственному значению задержки:
// ...
states: {
green: {
after: {
1000: [
{ target: 'yellow', cond: 'trafficIsLight' },
{ target: 'green' } // reenter 'green' state
]
}
}
}
// ...
Или отложенные переходы могут быть обусловлены несколькими задержками. Будет выполнен первый выбранный отложенный переход, который предотвратит выполнение последующих переходов. В следующем примере, если условие trafficIsLight
истинно, то более поздний переход 2000: 'yellow'
не будет выполняться:
// ...
states: {
green: {
after: {
1000: { target: 'yellow', cond: 'trafficIsLight' },
// always transition to 'yellow' after 2 seconds
2000: { target: 'yellow' }
}
}
}
// ...
Условные отложенные переходы также можно указать в виде массива:
// ...
states: {
green: {
after: [
{
delay: 1000,
target: 'yellow',
cond: 'trafficIsLight',
},
{ delay: 2000, target: 'yellow' },
];
}
}
// ...
Вычисляемые задержки на переходах¶
Начиная с версии 4.4+
Отложенные переходы, указанные в свойстве after: {...}
, могут иметь динамические задержки, задаваемые строковой ссылкой на задержку:
const lightDelayMachine = createMachine(
{
id: 'lightDelay',
initial: 'green',
context: {
trafficLevel: 'low',
},
states: {
green: {
after: {
// after 1 second, transition to yellow
LIGHT_DELAY: { target: 'yellow' },
},
},
yellow: {
after: {
YELLOW_LIGHT_DELAY: { target: 'red' },
},
},
// ...
},
},
{
// String delays configured here
delays: {
LIGHT_DELAY: (context, event) => {
return context.trafficLevel === 'low' ? 1000 : 3000;
},
YELLOW_LIGHT_DELAY: 500, // static value
},
}
);
Или напрямую функцией, как условные отложенные переходы:
// ...
green: {
after: [
{
delay: (context, event) => {
return context.trafficLevel === 'low' ? 1000 : 3000;
},
target: 'yellow'
}
]
},
// ...
Однако предпочтительнее использовать строковые ссылки на задержку, как в первом примере, или в свойстве задержки:
// ...
green: {
after: [
{
delay: 'LIGHT_DELAY',
target: 'yellow'
}
]
},
// ...
Отложенные события¶
Если вы просто хотите отправить событие после задержки, вы можете указать delay
в качестве опции во втором аргументе создателя действия send(...)
:
import { actions } from 'xstate';
const { send } = actions;
// action to send the 'TIMER' event after 1 second
const sendTimerAfter1Second = send(
{ type: 'TIMER' },
{ delay: 1000 }
);
Вы также можете предотвратить отправку этих отложенных событий, отменив их. Это делается с помощью создателя действия cancel(...)
:
import { actions } from 'xstate';
const { send, cancel } = actions;
// action to send the 'TIMER' event after 1 second
const sendTimerAfter1Second = send(
{ type: 'TIMER' },
{
delay: 1000,
id: 'oneSecondTimer', // give the event a unique ID
}
);
// pass the ID of event to cancel
const cancelTimer = cancel('oneSecondTimer');
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: {
entry: sendTimerAfter1Second,
on: {
TIMER: { target: 'active' },
CANCEL: { actions: cancelTimer },
},
},
active: {},
},
});
// if the CANCEL event is sent before 1 second,
// the TIMER event will be canceled.
Вычисляемые задержки¶
Начиная с версии 4.3+
Параметр delay
также может быть вычислен как выражение задержки, которое представляет собой функцию, которая принимает текущий контекст context
и событие event
, вызвавшее действие send()
, и возвращает вычисленный delay
(в миллисекундах):
const dynamicDelayMachine = createMachine({
id: 'dynamicDelay',
context: {
initialDelay: 1000,
},
initial: 'idle',
states: {
idle: {
on: {
ACTIVATE: { target: 'pending' },
},
},
pending: {
entry: send(
{ type: 'FINISH' },
{
// delay determined from custom event.wait property
delay: (context, event) =>
context.initialDelay + event.wait || 0,
}
),
on: {
FINISH: { target: 'finished' },
},
},
finished: { type: 'final' },
},
});
const dynamicDelayService = interpret(dynamicDelayMachine)
.onDone(() => console.log('done!'))
.start();
dynamicDelayService.send({
type: 'ACTIVATE',
// arbitrary property
wait: 2000,
});
// after 3000ms (1000 + 2000), console will log:
// => 'done!'
Интерпретация¶
С интерпретатором XState для отложенных действий будут использоваться собственные функции setTimeout
и clearTimeout
:
import { interpret } from 'xstate';
const service = interpret(
lightDelayMachine
).onTransition((state) => console.log(state.value));
service.start();
// => 'green'
// (after 1 second)
// => 'yellow'
Для тестирования интерпретатор XState предоставляет SimulatedClock
:
import { interpret } from 'xstate';
// import { SimulatedClock } from 'xstate/lib/interpreter'; // < 4.6.0
import { SimulatedClock } from 'xstate/lib/SimulatedClock'; // >= 4.6.0
const service = interpret(lightDelayMachine, {
clock: new SimulatedClock(),
}).onTransition((state) => console.log(state.value));
service.start();
// => 'green'
// move the SimulatedClock forward by 1 second
service.clock.increment(1000);
// => 'yellow'
Вы можете создать свои собственные «часы», чтобы предоставить их интерпретатору. Интерфейс часов - это объект с двумя функциями / методами:
setTimeout
— те же аргументы, что иwindow.setTimeout(fn, timeout)
clearTimeout
— те же аргументы, что иwindow.clearTimeout(id)
За кулисами¶
Свойство after: ...
не вносит ничего нового в семантику диаграммы состояний. Вместо этого он создает обычные переходы, которые выглядят следующим образом:
// ...
states: {
green: {
entry: [
send(after(1000, 'light.green'), { delay: 1000 }),
send(after(2000, 'light.green'), { delay: 2000 })
],
onExit: [
cancel(after(1000, 'light.green')),
cancel(after(2000, 'light.green'))
],
on: {
[after(1000, 'light.green')]: {
target: 'yellow',
cond: 'trafficIsLight'
},
[after(2000, 'light.green')]: {
target: 'yellow'
}
}
}
}
// ...
Интерпретируемая диаграмма состояний будет отправлять send(...)
события after(...)
после их задержки delay
, если только узел состояния не будет закрыт, что отменит cancel(...)
эти отложенные события send(...)
.