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
相关产品推荐
相关产品推荐

