You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React变速Clock组件实现问询:速度变更时避免立即重置setTimeout定时器

问题原因

你当前代码的问题根源是Clock组件每次渲染时,传给useInterval的第二个参数都是全新生成的匿名函数,useEffect检测到getDelay依赖变更就会立即执行清理逻辑销毁旧定时器,再创建新定时器,直接导致变速时节奏被打断。

调整方案

核心思路是把会频繁变化的回调、延迟获取函数都用ref存储,避免成为定时器副作用的依赖,仅在tick触发时读取最新值做校验:

  • 新增两个ref分别存储最新的tickCallback和getDelay
  • 用独立的轻量副作用同步回调、延迟获取函数到ref,该副作用不会影响定时器运行
  • 定时器useEffect仅保留isPlaying作为依赖,只有播放状态变更时才会重置定时器
  • 保留原有的tick时校验延迟、按需更换定时器的逻辑,确保变速仅在下一次tick后生效,节奏平滑
调整后完整代码
import React, { useEffect, useRef } from "react";

type Callback = () => void;

function useInterval(tickCallback: Callback, getDelay: () => number, isPlaying: boolean) {
    const refDelay = useRef<number>(getDelay());
    // 新增ref存储最新的回调和延迟获取函数
    const refTickCallback = useRef<Callback>(tickCallback);
    const refGetDelay = useRef<() => number>(getDelay);

    // 同步最新的tick回调到ref,不会影响定时器
    useEffect(() => {
        refTickCallback.current = tickCallback;
    }, [tickCallback]);

    // 同步最新的延迟获取函数到ref,不会影响定时器
    useEffect(() => {
        refGetDelay.current = getDelay;
    }, [getDelay]);

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

        function tick() {
            const newDelay = refGetDelay.current();
            const currentCallback = refTickCallback.current;
            if (currentCallback) {
                console.log(`newDelay: ${newDelay}`);
                currentCallback();
                if (newDelay !== refDelay.current) {
                    // 仅在tick时判断延迟变更,重置定时器
                    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);
    }, [isPlaying]); // 仅保留isPlaying作为依赖,变速不会触发该effect重跑
}

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 />);
}
补充说明

如果父组件传入的getNpm、callback是每次渲染生成的匿名函数,建议用useCallback包裹,可减少不必要的ref同步开销,即使不包裹也不会破坏定时器的平滑变速特性。


内容的提问来源于stack exchange,提问作者user103716

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 15:54:09