# 任务 4:计时器

¥Task 4: Timer

这是 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, some elapsed variable is incremented by some interval on every TICK event.

  • 始终检查 running 状态(瞬态转换)下的 elapsed 不超过 duration(保护转换)

    ¥Always check that elapsed does not exceed duration (guarded transition) in the running state (transient transition)

    • 如果 elapsed 超过 duration,则转换到 paused 状态。

      ¥If elapsed exceeds duration, transition to the paused state.

  • 始终检查 paused 状态下的 duration 是否不超过 elapsed(受保护的转换)。

    ¥Always check that duration does not exceed elapsed (guarded transition) in the paused state.

    • 如果 duration 超过 elapsed,则转换到 running 状态。

      ¥If duration exceeds elapsed, transition to the running state.

  • duration 始终可以通过某些 DURATION.UPDATE 事件进行更新。

    ¥The duration can always be updated via some DURATION.UPDATE event.

  • RESET 事件将 elapsed 重置为 0

    ¥A RESET event resets elapsed to 0.

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.UPDATETICKRESET 等,该接口是完全响应式和并发的。它还简化了实现。

¥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, receiving TICK events from some invoked interval service, and updating context.elapsed.

  • "paused" - 计时器未运行且不再接收 TICK 事件的状态。

    ¥"paused" - the state where the timer is not running and no longer receiving TICK 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