带有 setTimeout 的 React,变速时钟

React, variable speed Clock with setTimeout

目标:创建一个Clock定时调用回调方法,速度可控的组件

棘手的部分:当速度改变时不要立即重置时钟计时器,但在下一个“滴答”检查所需的速度,如果它有变化,重置当前间隔并安排一个新的。这是在更改速度时保持时钟票平稳运行所必需的。

我认为传递一个 getDelay 函数 returns 延迟(而不是延迟本身的值)可以使这项工作成功,但事实并非如此。

如果我让 useEffect 跟踪 getDelay 函数,它将在延迟更改时重置。如果它不跟踪 getDelay 当时钟为 运行.

时速度不会改变
import React, { useEffect, useRef } from "react";

type Callback = () => void;

function useInterval(tickCallback: Callback, getDelay: () => number, isPlaying: boolean) {
    const refDelay = useRef<number>(getDelay());

    useEffect(() => {
        let id: number;
        console.log(`run useEffects`);

        function tick() {
            const newDelay = getDelay();
            if (tickCallback) {
                console.log(`newDelay: ${newDelay}`);
                tickCallback();
                if (newDelay !== refDelay.current) {
                    // if delay has changed, clear and schedule new interval
                    console.log(`delay changed. was ${refDelay.current} now is ${newDelay}`)
                    refDelay.current = newDelay;
                    clear();
                    playAndSchedule(newDelay);
                }
            }
        }
        
        /** clear interval, if any */
        function clear() {
            if (id) {
                console.log(`clear ${id}`)
                clearInterval(id);
            }
        }

        /** schedule interval and return cleanup function */
        function playAndSchedule(delay: number) {
            if (isPlaying) {
                id = window.setInterval(tick, delay);
                console.log(`schedule delay id ${id}. ms ${delay}`)
                return clear
            }
        }
        return playAndSchedule(refDelay.current);
    },
        // with getDelay here the clock is reset as soon as the delay value changes
        [isPlaying, getDelay]);
}

type ClockProps = {
    /** true if playing */
    isPlaying: boolean;

    /** return the current notes per minute */
    getNpm: () => number;

    /** function to be executed every tick */
    callback: () => void;
}

export function Clock(props: ClockProps) {
    const { isPlaying, getNpm, callback } = props;

    useInterval(
        callback,
        () => {
            console.log(`compute delay for npm ${getNpm()}`);
            return 60_000 / getNpm();
        },
        isPlaying);

    return (<React.Fragment />);
}

你可以使用这样的东西:

import React, { useCallback, useEffect, useMemo, useRef } from 'react';

function useInterval(tickCallback: () => void, delay: number, isPlaying: boolean) {
  const timeout = useRef<any>(null);
  const savedDelay = useRef(delay);
  const savedTickCallback = useRef(tickCallback);

  useEffect(() => {
    savedDelay.current = delay;
  }, [delay])

  useEffect(() => {
    savedTickCallback.current = tickCallback;
  }, [tickCallback])

  const startTimeout = useCallback(() => {
    const delay = savedDelay.current;
    console.log('next delay', delay);
    timeout.current = setTimeout(() => {
      console.log('delay done', delay);
      savedTickCallback.current();
      startTimeout();
    }, savedDelay.current);
  }, []);

  useEffect(() => {
      if (isPlaying) {
        if (!timeout.current) {
          startTimeout();
        }
      } else {
        if (timeout.current) {
          clearTimeout(timeout.current);
        }
      }
    },
    [isPlaying, startTimeout],
  );
}

type ClockProps = {
  /** true if playing */
  isPlaying: boolean;

  /** return the current notes per minute */
  getNpm: () => number;

  /** function to be executed every tick */
  callback: () => void;
}

export const Clock: React.FC<ClockProps> = ({ isPlaying, getNpm, callback }) => {

  const delay = useMemo(() => {
    console.log(`compute delay for npm ${getNpm()}`);
    return 60_000 / getNpm();
  }, [getNpm]);

  useInterval(callback, delay, isPlaying);

  return null;
};