# 任务 4:计时器
¥Task 4: Timer
这些 XState v4 文档不再维护
XState v5 现已推出!阅读有关 XState v5 的更多信息 (opens new window) 和 查看 XState v5 文档 (opens new window)。
¥XState v5 is out now! Read more about XState v5 (opens new window) and check out the XState v5 docs (opens new window).
这是 7GUI 的 7 项任务 (opens new window) 的第四个:
¥This is the fourth of The 7 Tasks from 7GUIs (opens new window):
挑战:并发性、竞争性用户/信号交互、响应能力。
¥Challenges: concurrency, competing user/signal interactions, responsiveness.
任务是构建一个框架,其中包含一个用于显示经过时间 e 的仪表 G、一个将经过时间显示为数值的标签、一个滑块 S(在计时器运行时可以通过滑块 S 调整计时器的持续时间 d)以及一个 重置按钮 R。调整 S 必须立即反映在 d 上,而不仅仅是在 S 被释放时。因此,当移动 S 时,G 的填充量(通常)会立即改变。当 e ≥ d 为真时,计时器停止(并且 G 将满)。此后,如果 d 增加,使得 d > e 为真,则计时器重新开始计时,直到 e ≥ d 再次为真。单击 R 会将 e 重置为零。
¥The task is to build a frame containing a gauge G for the elapsed time e, a label which shows the elapsed time as a numerical value, a slider S by which the duration d of the timer can be adjusted while the timer is running and a reset button R. Adjusting S must immediately reflect on d and not only when S is released. It follows that while moving S the filled amount of G will (usually) change immediately. When e ≥ d is true then the timer stops (and G will be full). If, thereafter, d is increased such that d > e will be true then the timer restarts to tick until e ≥ d is true again. Clicking R will reset e to zero.
计时器处理并发性是指更新已用时间的计时器进程与用户与 GUI 应用的交互同时运行。这也意味着竞争用户和信号交互的解决方案已经过测试。滑块调整必须立即反映这一事实还测试了解决方案的响应能力。一个好的解决方案将明确该信号是一个计时器滴答声,并且一如既往,没有太多的脚手架。
¥Timer deals with concurrency in the sense that a timer process that updates the elapsed time runs concurrently to the user’s interactions with the GUI application. This also means that the solution to competing user and signal interactions is tested. The fact that slider adjustments must be reflected immediately moreover tests the responsiveness of the solution. A good solution will make it clear that the signal is a timer tick and, as always, has not much scaffolding.
Timer 的灵感直接来自论文 跨越州界线:使面向对象的框架适应函数式反应语言 (opens new window) 中的定时器示例。
¥Timer is directly inspired by the timer example in the paper Crossing State Lines: Adapting Object-Oriented Frameworks to Functional Reactive Languages (opens new window).
# 建模
¥Modeling
对该计时器建模的关键点在于描述本身:
¥The key point in modeling this timer is in the description itself:
一个好的解决方案将明确该信号是计时器滴答声
¥A good solution will make it clear that the signal is a timer tick
事实上,我们可以将计时器滴答建模为更新某些父计时器机器的上下文的信号(事件)。计时器可以处于 paused
状态或 running
状态,并且这些计时器滴答声理想情况下应仅在机器处于 running
状态时才处于活动状态。这为我们如何建模其他需求奠定了良好的基础:
¥Indeed, we can model timer ticks as a signal (event) that updates the context of some parent timer machine. The timer can be in either the paused
state or the running
state, and these timer ticks should ideally only be active when the machine is in the running
state. This gives us a good basis for how we can model the other requirements:
当处于
running
状态时,某些elapsed
变量在每个TICK
事件上都会增加一些interval
。¥When in the
running
state, someelapsed
variable is incremented by someinterval
on everyTICK
event.始终检查
running
状态(瞬态转换)下的elapsed
不超过duration
(保护转换)¥Always check that
elapsed
does not exceedduration
(guarded transition) in therunning
state (transient transition)如果
elapsed
超过duration
,则转换到paused
状态。¥If
elapsed
exceedsduration
, transition to thepaused
state.
始终检查
paused
状态下的duration
是否不超过elapsed
(受保护的转换)。¥Always check that
duration
does not exceedelapsed
(guarded transition) in thepaused
state.如果
duration
超过elapsed
,则转换到running
状态。¥If
duration
exceedselapsed
, transition to therunning
state.
duration
始终可以通过某些DURATION.UPDATE
事件进行更新。¥The
duration
can always be updated via someDURATION.UPDATE
event.RESET
事件将elapsed
重置为0
。¥A
RESET
event resetselapsed
to0
.
在 running
状态下,我们可以调用一个服务,该服务调用 setInterval(...)
在所需的 interval
上发送 TICK
事件。
¥In the running
state, we can invoke a service that calls setInterval(...)
to send a TICK
event on the desired interval
.
通过将所有事物建模为 "signal"(事件),例如 DURATION.UPDATE
、TICK
、RESET
等,该接口是完全响应式和并发的。它还简化了实现。
¥By modeling everything as a "signal" (event), such as DURATION.UPDATE
, TICK
, RESET
, etc., the interface is fully reactive and concurrent. It also simplifies the implementation.
状态:
¥States:
"running"
- 计时器正在运行、从某些调用的间隔服务接收TICK
事件并更新context.elapsed
的状态。¥
"running"
- the state where the timer is running, receivingTICK
events from some invoked interval service, and updatingcontext.elapsed
."paused"
- 计时器未运行且不再接收TICK
事件的状态。¥
"paused"
- the state where the timer is not running and no longer receivingTICK
events.
上下文:
¥Context:
interface TimerContext {
// The elapsed time (in seconds)
elapsed: number;
// The maximum time (in seconds)
duration: number;
// The interval to send TICK events (in seconds)
interval: number;
}
事件:
¥Events:
type TimerEvent =
| {
// The TICK event sent by the spawned interval service
type: 'TICK';
}
| {
// User intent to update the duration
type: 'DURATION.UPDATE';
value: number;
}
| {
// User intent to reset the elapsed time to 0
type: 'RESET';
};
# 编码
¥Coding
export const timerMachine = createMachine({
initial: 'running',
context: {
elapsed: 0,
duration: 5,
interval: 0.1
},
states: {
running: {
invoke: {
src: (context) => (cb) => {
const interval = setInterval(() => {
cb('TICK');
}, 1000 * context.interval);
return () => {
clearInterval(interval);
};
}
},
on: {
'': {
target: 'paused',
cond: (context) => {
return context.elapsed >= context.duration;
}
},
TICK: {
actions: assign({
elapsed: (context) =>
+(context.elapsed + context.interval).toFixed(2)
})
}
}
},
paused: {
on: {
'': {
target: 'running',
cond: (context) => context.elapsed < context.duration
}
}
}
},
on: {
'DURATION.UPDATE': {
actions: assign({
duration: (_, event) => event.value
})
},
RESET: {
actions: assign({
elapsed: 0
})
}
}
});
# 结果
¥Result