کرونومتر – React – انجمن DEV
یک ویجت کرونومتر بسازید که می تواند مدت زمان گذشته را اندازه گیری کند. تایمر فعلی را نشان می دهد و دو دکمه در زیر دارد: “شروع/توقف” و “بازنشانی”.
الزامات
- دکمه شروع/توقف: بسته به اینکه تایمر در حال اجرا باشد، تایمر را شروع یا متوقف می کند.
- بازنشانی: تایمر را به 0 بازنشانی می کند و تایمر را متوقف می کند.
- تایمر تعداد ثانیه های سپری شده را تا میلی ثانیه نشان می دهد.
- با کلیک بر روی تایمر باید تایمر شروع یا متوقف شود. برچسب دکمه Start/Stop نیز باید بر این اساس به روز شود.
- برای فرمت زمان نمایش با فرمت hh:mm:ss:ms یک گزینه اضافی اختیاری خوب است.
شما آزاد هستید که خلاقیت خود را به کار بگیرید تا ظاهر کرونومتر را طراحی کنید. میتوانید ویجت کرونومتر Google را برای الهام گرفتن و یک مثال امتحان کنید.
راه حل:
این سوال در نگاه اول ساده به نظر می رسد اما در واقع پیچیده تر از آن چیزی است که به نظر می رسد. توجه داشته باشید که پارامتر تاخیر setInterval قابل اعتماد نیست. مدت زمانی واقعی که بین تماسها و تماسهای برگشتی میگذرد به دلایل مختلف ممکن است بیشتر از تأخیر داده شده باشد. به دلیل این رفتار، نمیتوانیم فرض کنیم که هر بار بازخوانی بازهای فعال میشود، همان مدت زمانی که گذشت. برای اطمینان از اینکه از بهروزترین زمانبندیها استفاده میکنیم، باید زمان فعلی را در کد پاسخ به تماس بخوانیم.
حالت
بخش مشکل این سوال این است که تصمیم بگیرید که چه چیزی در حالت کامپوننت قرار می گیرد و چگونه آنها را مدیریت کنید. ما به چند حالت نیاز داریم:
totalDuration
: مجموع زمانی که تا کنون گذشته است.timerId
: شناسه تایمر تایمر بازه زمانی فعلی یاnull
اگر در حال حاضر هیچ تایمر در حال اجرا وجود ندارد.lastTickTiming
: این زمانی است که آخرین بازه تماس مجدد اجرا شده است. ما به افزایش آن ادامه خواهیم دادtotalDuration
توسط دلتا بین زمان جاری (Date.now()
) وlastTickTiming
. با استفاده از این رویکرد،totalDuration
حتی اگر تماس ها در فواصل نامنظم اجرا شوند، همچنان دقیق خواهند بود. ما استفاده می کنیمuseRef
برای ایجاد این مقدار زیرا در کد رندر استفاده نشده است.
از آنجایی که چند دکمه در نیازمندیها وجود دارد که عملکرد تکراری دارند، باید این عملکردها را به عنوان چند عملکرد که توسط دکمهها فعال میشوند تعریف کنیم:
startTimer
این عملکرد تایمر را خاموش می کند و آن را به روز می کند totalDuration
ارزش هر بار setInterval
تماس مجدد با دلتا بین آخرین زمان به روز رسانی اجرا می شود (lastTickTiming
) و زمان فعلی. ما از یک زمان بندی فاصله ای استفاده می کنیم 1ms
از آنجایی که کرونومترها بسیار حساس به زمان هستند و دقت در سطح میلی ثانیه مورد نظر است.
stopInterval
یک عملکرد ساده برای جلوگیری از اجرای تایمر فاصله (از طریق clearInterval
) و جریان را پاک کنید timerId
. این توسط دکمه “Stop” و “Reset” استفاده می شود.
resetTimer
می خواهیم در این تابع کامپوننت را به حالت اولیه بازگردانیم. با تماس، تایمر فاصله را متوقف می کند stopInterval()
و همچنین کل مدت زمان را به 0 بازنشانی می کند. تنظیم مجدد مقدار مهم نیست lastTickTiming
زیرا در ابتدای تنظیم خواهد شدstartTimer()
، قبل از اجرای اولین تماس بازه ای. توسط دکمه “تنظیم مجدد” استفاده می شود.
toggleTimer
تابعی برای جابهجایی بین تماس stopInterval()
و startTimer()
بسته به اینکه آیا تایمر فعلی وجود دارد یا خیر. توسط نمایش زمان و دکمه “شروع”https://dev.to/”Stop” استفاده می شود.
دسترسی
افرادی که با a11y آشنایی ندارند یک رویداد onClick/’click’ به عنصر DOM اضافه می کنند که زمان را ارائه می کند (معمولاً
) و آن را کامل در نظر بگیرید. با این حال، این تایمر فقط با انجام این کار برای 11y مناسب نیست. برخی ممکن است اضافه کنند tabIndex="0"
(برای اجازه دادن به تمرکز) و role="button"
به عنصری که مطمئنا a11y را بهبود می بخشد، اما بهترین نیست.
برای بهترین a11y، می توانیم و باید از a استفاده کنیم <button>
برای ارائه زمان بندی، که با مزایای اضافی a11y مانند فوکوس و پشتیبانی از صفحه کلید همراه است. با استفاده از a <button>
، از فوکوس خودکار پشتیبانی می کنید (می توانید از Tab برای فوکوس روی تایمر استفاده کنید) و پشتیبانی از صفحه کلید (برای شروع/ توقف تایمر روی Spacebar ضربه بزنید). دومی بدون کد سفارشی برای افزودن شنوندگان رویدادهای کلیدی به عناصر غیر تعاملی امکان پذیر نخواهد بود.
تجربه ی کاربرuser-select
: هیچ کدام به تایمر اضافه نمی شود تا اگر کاربر روی آنها دوبار کلیک کند، ارقام انتخاب نمی شوند. معمولاً انتخاب ارقام مورد نظر نیست.
StopWatchWrapper.js
import React from 'react';
import { StopWatch } from './StopWatch';
export const StopWatchWrapper = () => {
return <StopWatch />;
};
StopWatch.js
import React, { useState, useRef } from 'react';
import './stopwatch.css';
const MS_IN_SECOND = 1000;
const SECONDS_IN_MINUTE = 60;
const MINUTES_IN_HOUR = 60;
const MS_IN_HOUR = MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MS_IN_SECOND;
const MS_IN_MINUTE = SECONDS_IN_MINUTE * MS_IN_SECOND;
const padTwoDigit = (number) => (number >= 10 ? String(number) : `0${number}`);
/* key point is this function: */
const formatTime = (timeParam) => {
let time = timeParam;
const parts = {
hours: 0,
minutes: 0,
seconds: 0,
ms: 0,
};
if (time > MS_IN_HOUR) {
parts.hours = Math.floor(time / MS_IN_HOUR);
time %= MS_IN_HOUR;
}
if (time > MS_IN_MINUTE) {
parts.minutes = Math.floor(time / MS_IN_MINUTE);
time %= MS_IN_MINUTE;
}
if (time > MS_IN_SECOND) {
parts.seconds = Math.floor(time / MS_IN_SECOND);
time %= MS_IN_SECOND;
}
parts.ms = time;
return parts;
};
export const StopWatch = () => {
const lastTickTiming = useRef(null); // use `useRef` to create this value since it's not used in the render code.
const [totalDuration, setTotalDuration] = useState(0);
const [timerId, setTimerId] = useState(null); // Timer ID of the active interval, if one is running.
const isRunning = timerId != null; // Derived state to determine if there's a timer running.
const startTimer = () => {
lastTickTiming.current = Date.now(); // 用于记录上次setInterval的callback停在哪儿了(记录了ms)
//下面这个setInterval一直run, 每隔1ms run一次callback, 然后根据 之前的现在的时间-上次记录的ms = passedtime, 再去更新totalDuration
const timer = window.setInterval(() => {
const now = Date.now();
const timePassed = now - lastTickTiming.current;
// Use the callback form of setState to ensure we are using the latest value of duration.
setTotalDuration((duration) => duration + timePassed);
lastTickTiming.current = now; // update lastTickTiming
}, 1);
setTimerId(timer);
};
const stopInterval = () => {
window.clearInterval(timerId);
setTimerId(null);
};
const resetTimer = () => {
stopInterval();
setTotalDuration(0);
};
const toggleTimer = () => {
if (isRunning) stopInterval();
else startTimer();
};
const formattedTime = formatTime(totalDuration);
// console.log(formattedTime);
return (
<div>
<button
className="time"
onClick={() => {
toggleTimer();
}}
>
{formattedTime.hours > 0 && (
<span>
<span className="time-number">{formattedTime.hours}</span>
<span className="time-unit">h</span>
</span>
)}
{formattedTime.minutes > 0 && (
<span>
<span className="time-number">{formattedTime.minutes}</span>
<span className="time-unit">m</span>
</span>
)}
<span>
<span className="time-number">{formattedTime.seconds}</span>
<span className="time-unit">s</span>
</span>
<span className="time-number time-number--small">
{padTwoDigit(Math.floor(formattedTime.ms / 10))}
</span>
</button>
<div>
<button
onClick={() => {
toggleTimer();
}}
>
{isRunning ? 'Stop' : 'Start'}
</button>
<button
onClick={() => {
resetTimer();
}}
>
Reset
</button>
</div>
</div>
);
};
timewatch.css
.time {
align-items: baseline;
background-color: transparent;
border: none;
cursor: pointer;
display: flex;
gap: 16px;
user-select: none;
}
.time-unit {
font-size: 24px
}
.time-number {
font-size: 62px;
}
.time-number--small {
font-size: 36px;
}
توجه: منطق بالا می تواند برای شمارش معکوس تایمر نیز کار کند. مهمترین چیزی که باید تغییر کند عبارتند از:
1.const [totalDuration, setTotalDuration] = useState(maxMinutes * MS_IN_MINUTE);
، maxMinutes
لوازم مولفه تایمر است که می تواند با اراده کاربر تصمیم گیری کند.
2.setTotalDuration((duration) => duration - timePassed)
. روش محاسبه مدت زمان کل تغییر کرده است.