From 856413d6f55edc9eae622e08c7c392871478f908 Mon Sep 17 00:00:00 2001 From: Eksiart Date: Tue, 1 Jul 2025 23:56:14 +0800 Subject: [PATCH 1/2] =?UTF-8?q?main=20=F0=9F=A7=8A=20refactor(useStopwatch?= =?UTF-8?q?):=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=20?= =?UTF-8?q?=D1=85=D1=83=D0=BA=20=D1=81=20=D0=BC=D0=B8=D0=BB=D0=BB=D0=B8?= =?UTF-8?q?=D1=81=D0=B5=D0=BA=D1=83=D0=BD=D0=B4=D0=BD=D0=BE=D0=B9=20=D1=82?= =?UTF-8?q?=D0=BE=D1=87=D0=BD=D0=BE=D1=81=D1=82=D1=8C=D1=8E=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=B0=D0=B8=D0=B2=D0=B0=D0=B5?= =?UTF-8?q?=D0=BC=D0=BE=D0=B3=D0=BE=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=B0=D0=BB=D0=B0=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлена поддержка миллисекунд в расчётах времени - Заменена логика setInterval на useInterval из библиотеки - Добавлен параметр updateInterval для гибкой настройки частоты обновления - Улучшена точность отсчёта за счёт расчёта времени по разнице timestamp - Добавлена поддержка RequestAnimationFrame --- .../bundle/hooks/useStopwatch/useStopwatch.js | 129 +++++++------- .../hooks/useStopwatch/useStopwatch.demo.tsx | 7 +- .../src/hooks/useStopwatch/useStopwatch.ts | 158 ++++++++++-------- 3 files changed, 164 insertions(+), 130 deletions(-) diff --git a/packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js b/packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js index 1aa9e62e..997ac56c 100644 --- a/packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js +++ b/packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js @@ -1,19 +1,25 @@ -import { useEffect, useState } from 'react'; -const getStopwatchTime = (time) => { - if (!time) +import { useState } from 'react'; +import { useInterval } from '../useInterval/useInterval'; +import { useRaf } from '../useRaf/useRaf'; +const getStopwatchTime = (count) => { + if (!count) return { days: 0, hours: 0, minutes: 0, seconds: 0, + milliseconds: 0, count: 0 }; - const days = Math.floor(time / 86400); - const hours = Math.floor((time % 86400) / 3600); - const minutes = Math.floor((time % 3600) / 60); - const seconds = Math.floor(time % 60); - return { days, hours, minutes, seconds, count: time }; + const totalSeconds = Math.floor(count / 1000); + const days = Math.floor(totalSeconds / 86400); + const hours = Math.floor((totalSeconds % 86400) / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = Math.floor(totalSeconds % 60); + const milliseconds = count % 1000; + return { days, hours, minutes, seconds, milliseconds, count }; }; +const getMillsDiffOrZero = (millis) => (Date.now() - millis > 0 ? Date.now() - millis : 0); /** * @name useStopwatch * @description - Hook that creates a stopwatch functionality @@ -22,72 +28,75 @@ const getStopwatchTime = (time) => { * @overload * @param {number} [initialTime=0] The initial time of the timer * @param {boolean} [options.enabled=true] The enabled state of the timer + * @param {number} [options.updateInterval=1000] The update interval of the timer + * @param {boolean} [options.useRaf=false] Use RAF instead of setInterval (updateInterval will be ignored) * @returns {UseStopwatchReturn} An object containing the current time and functions to interact with the timer * * @example - * const { seconds, minutes, start, pause, reset } = useStopwatch(1000, { enabled: false }); + * const { seconds, minutes, start, pause, reset } = useStopwatch(1000, { enabled: false, updateInterval: 1000 }); * * @overload * @param {number} [options.initialTime=0] -The initial time of the timer * @param {boolean} [options.enabled=true] The enabled state of the timer + * @param {number} [options.updateInterval=1000] The update interval of the timer + * @param {boolean} [options.useRaf=false] Use RAF instead of setInterval (updateInterval will be ignored) * @returns {UseStopwatchReturn} An object containing the current time and functions to interact with the timer * * @example - * const { seconds, minutes, start, pause, reset } = useStopwatch({ initialTime: 1000, enabled: false }); + * const { seconds, minutes, start, pause, reset } = useStopwatch({ initialTime: 1000, enabled: false, updateInterval: 1000 }); */ export const useStopwatch = (...params) => { const initialTime = (typeof params[0] === 'number' ? params[0] : params[0]?.initialTime) ?? 0; const options = typeof params[0] === 'number' ? params[1] : params[0]; - const [time, setTime] = useState(getStopwatchTime(initialTime)); - const [paused, setPaused] = useState(!options?.enabled); - useEffect(() => { - if (paused) return; - const onInterval = () => { - setTime((prevTime) => { - const updatedCount = prevTime.count + 1; - if (updatedCount % 60 === 0) { - return { - ...prevTime, - minutes: prevTime.minutes + 1, - seconds: 0, - count: updatedCount - }; - } - if (updatedCount % (60 * 60) === 0) { - return { - ...prevTime, - hours: prevTime.hours + 1, - minutes: 0, - seconds: 0, - count: updatedCount - }; - } - if (updatedCount % (60 * 60 * 24) === 0) { - return { - ...prevTime, - days: prevTime.days + 1, - hours: 0, - minutes: 0, - seconds: 0, - count: updatedCount - }; - } - return { - ...prevTime, - seconds: prevTime.seconds + 1, - count: updatedCount - }; - }); - }; - const interval = setInterval(() => onInterval(), 1000); - return () => clearInterval(interval); - }, [paused]); + const smooth = options?.useRaf ?? false; + const enabled = options?.enabled ?? true; + const updateInterval = options?.updateInterval ?? 1000; + const [milliseconds, setMilliseconds] = useState(initialTime); + const [timestamp, setTimestamp] = useState(Date.now() - initialTime); + const interval = useInterval( + () => setMilliseconds(getMillsDiffOrZero(timestamp)), + updateInterval, + { + immediately: enabled && !smooth + } + ); + const raf = useRaf(() => setMilliseconds(getMillsDiffOrZero(timestamp)), { + enabled: enabled && smooth + }); + const isRunning = smooth ? raf.active : interval.active; + const start = () => { + if (isRunning) return; + setTimestamp(new Date().getTime() - milliseconds); + if (smooth) { + raf.resume(); + } else { + interval.resume(); + } + }; + const pause = () => { + if (!isRunning) return; + setMilliseconds(getMillsDiffOrZero(timestamp)); + if (smooth) { + raf.pause(); + } else { + interval.pause(); + } + }; + const reset = () => { + setMilliseconds(initialTime); + setTimestamp(Date.now() - initialTime); + if (smooth) { + raf.resume(); + } else { + interval.resume(); + } + }; return { - ...time, - paused, - pause: () => setPaused(true), - start: () => setPaused(false), - reset: () => setTime(getStopwatchTime(initialTime)), - toggle: () => setPaused((prevPause) => !prevPause) + ...getStopwatchTime(milliseconds), + paused: !isRunning, + pause, + start, + reset, + toggle: () => (isRunning ? pause() : start()) }; }; diff --git a/packages/core/src/hooks/useStopwatch/useStopwatch.demo.tsx b/packages/core/src/hooks/useStopwatch/useStopwatch.demo.tsx index 2dd772cc..18376018 100644 --- a/packages/core/src/hooks/useStopwatch/useStopwatch.demo.tsx +++ b/packages/core/src/hooks/useStopwatch/useStopwatch.demo.tsx @@ -1,12 +1,15 @@ import { useStopwatch } from '@siberiacancode/reactuse'; const Demo = () => { - const stopwatch = useStopwatch(); + const stopwatch = useStopwatch({ + updateInterval: 100 + }); return (

- {stopwatch.minutes} m:{stopwatch.seconds} s + {stopwatch.minutes} m:{stopwatch.seconds} s: + {String(stopwatch.milliseconds).padStart(3, '0')} ms