Галоўная / Артыкулы

Таймеры аблікавання часу без дрейфу ў React: ад setTimeout да чыстага CSS

Парульце setTimeout, requestAnimationFrame і тэхніку CSS без JavaScript для аблікавання часу ў React, уключаючы трік адна-задзейсцавання, який падтрымвае сінхроннасць цифраў.

Выдавец: REACTAPP.TOP17 хв чытання4011 слоў

Крыніцы: How I Build a Countdown Timer in React

Карточка-ваучэр з датай заканчыць чыннасці павінна мець видны гадзіннік: гадзіны, хвіліны і секунды, якія спускаюцца да моменту, калі акцыя зникае. Цей элемент ўсьмохнуты, але він мае великое значэнне для продукту. Таймер, який запаздывае за рэальным часам, раптова прыскаквае на две секунды або продовжуе тиха працаваць вентылятору ноутбука, падрывае довер'е корыстувальніка да всього навакол. У гэтым кялешчыку практычна показана трохі спосабаў стварэння такога зворачнага лічыльніка ў React, адносна пояснены прычыны, чаму першы спосаб запаздывае, і практычна показана тэхніка, якая выкорыстоўвае толькі CSS і можа кераваць сотнямі таймероў без жаднага JavaScript на кожны прыскок.

Проблема ў адной фразе

Кожны ваучэр мае значэнне claim_end_time. На экране павінна быць паказана колькі часу застаўся да гэтага моменту, у формате HH:MM:SS, якае апдэйтаваецца кожную секунду, і ён павінен зупыніцца на нуле, калі ваучэр заканчывае свою чыннасць.

Арыметыка тут простая. Складным ёй являецца выбар як і калі яе перасчытаць, і самэўжо гэты выбар вялікай меры паўтарае точнасць, выкарыстоўванне CPU і тое, што можна зрабіць, калі заканчываецца час.

Падход 1: ланцуг setTimeout і прычыны його адхілення

Інстынктыўная рэалізацыя — это функцыя, якая апдэйтуе прадставленне даных і пасля чаго знову запланавае свое выконанне чераз адну секунду.

function tick() {
  updateDisplay();
  setTimeout(tick, 1000);
}
tick();

Тэкст чытаецца легка і, судзячы з быстрых тэстаў, працуе правільна. Проблема крыўцяеся ў спосабе выдавання таймераў. JavaScript на сторонцы запускаецца на адной галоўнай вялі, а цыкл змаганняў (event loop) дастае ёй адну задачу за раз. Калебак-функцыя таймера ёсць проста ўзначальнай задачай, якая стоіць у черге пасля обробнікав клікаў, калебак-функцыяў сеті, задач на адрасаванне і ўсьго таго, што React прагледвае. Значэнне 1000, якое вы пасылайце, ёсць мінімумам, а не гарантаваным часам. Якщо вялі зайнята на момент спылу секунды, калебак чакае, і ён можа быць выкананы пасля 1 050 мс або 1 200 мс. Якщо хочаце падтвердзіць, чаму галоўная вялі паводзіцца так, прачытайце нашу статыю Пра адну вялі JavaScript у працэсе браузера з калькольніка мнагах працэсаў.

Тое, што ператыя запазджэння прыводзіць да значных адхылэнняў, — гэта ланцоўка запуску. Кожны тык плануе наступны тык усередзіне сябе, таму будзь-якое запазджэнне пераходзіць да всіх наступных тыкаў. Нічога не можа вернуць гэтую ланцоўку да стандартнага часу. Запускайце такі цыкл на дзесять хвілін, паўпоруч зь ім выкарыстоўваючы Date.now(), і вы пабачыце, што ён адстае.

Таймер, які зменшае лічылку пасля кожнаго тыка, вимервае колькісць вызываў функцый, а не прайшоў час, а обмежэння колькісці вікнаў у фоне ўсё больш збяльшае гэты разлік.

Вы можете компенсаваць гэта, вимерваючы, наскількі пазнела кожная функцыя і пры неабходнасці скрачваючы наступны час запуску. Гэта працуе, але тады вам даведзяцца підтрымваць логіку корэкцыі, якая працуе проты платформы, а не за ёю. У будзь-якім случае код все равно выконваецца на галоўнай ніті кожную секунду.

Падход 2: requestAnimationFrame і перысчытанне на адной з часоў

requestAnimationFrame (rAF) просіць браузер вызваць вашу функцыю непраздлейшая да наступнага перамальвання, якое на большынці дасканоў вядзецца пра 60 разоў за секунду. Аднак частота не ёсць галоўным моментам. Галоўная змяна ёсць канцэптуальная: вы перестаеце спытваць «Чы не прыйшла ўжо наступная секунда?» і замест таго спытваеце «Скількі часу застаўся самэ пры гэтым моменте?»

function updateTime() {
  const now = Date.now();
  const remaining = endTime - now;
  if (remaining <= 0) {
    display('00:00:00');
    return;
  }
  display(format(remaining));
  requestAnimationFrame(updateTime);
}
requestAnimationFrame(updateTime);

У кожнай кадре залишчыковы час вырахоўваны з нуля, прычым Date.now() выступае як едынага адпаведнага джерага. Не існуе лічыльніка кадраў, не є ніякіх сумаваных інтэрвалоў. Якщо яны прыходзяць пазней, наступны кадр проста зноў чытае час і паказвае правильную значэнне. Памылкі не можу накаплявацца, таму што з аднаго кадра да іншага пераносіцца толькі сам час завершэння. Той жа прынцып дапамагае і у версіі з setTimeout: якщо кожны калебак расчыляе значэнне з Date.now() узамен на зменшэння лічыльніка, паказанне сама корэктуецца нават калі калебакі прыходзяць пазней.

Разумная працовая: виджыбальная значэнне зміняецца толькі раз на секунду, то чаму трэба выкананыя 60 разоў на секунду? У React дорогі ўчастак — не сама абяцанне, а павтарна нараджэння элемента, якая запускаецца за дапамогою setState. Таму цикл практыкуе порэвання на кожным кадрі і апдейтуе стацыю толькі тады, калі значэнне за цэлую секунду дзе-небудзь зміняецца.

Апдэйт стацыі толькі калі змінюецца секунда

Гэтая ідея реалізавана ўнутрь компонента. Заўважыце, што endTime — это Unix-таймстамп у секундах, і самэ гэта прычына, чаму яго прадаюць на 1000 перад тым, як порэвануць з Date.now().

const updateTime = (): void => {
  const now = Date.now();
  const timeTillStop = endTime * 1000 - now;
  if (timeTillStop <= 0) {
    setTimeLeft({ hours: 0, minutes: 0, seconds: 0 });
    onNoCountdownTimeLeft?.();
    return;
  }
  const totalSeconds = Math.floor(timeTillStop / 1000);
  if (totalSeconds !== lastSecondsRef.current) {
    lastSecondsRef.current = totalSeconds;
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;
    setTimeLeft({ hours, minutes, seconds });
  }
  animationRef.current = requestAnimationFrame(updateTime);
};

Два рефы виконваюць основную работу. lastSecondsRef зберагае апошнюй секундзе, яка была адрасавана. Паколькі ён являецца рэфам, запіс у яго ніколі не спрычынае адрасавання; це проста памяцевая ячэйка, яка застаецца між кадрамі. Таму цикл выконваецца з частотай кадраў, тады как React адрасавае максымум раз у секунду. Функцыя-вызов onNoCountdownTimeLeft, яка ўваходзіць у склад необавязковых параметраў, вызываецца, калі час заканчываецца, і дае роднім компонентам можлівасць сховаць картку, падзеяць даны занова або паказаць статус „заканчылася тэрмінавае дата“.

Другі рэф, animationRef, зберагае ID, які вяртае requestAnimationFrame. Функцыя для чысткі праблем вызывае яго, каб анулюваць чакаючы кадр, калі компонент адключаецца з мэнеджера вікна або зменяецца endTime:

return () => {
  if (animationRef.current) {
    cancelAnimationFrame(animationRef.current);
  }
};

Без такой чысткі, цикл будзе продовжваць работаць пасля таго, як корыстнік адыходзіць, вызываючы setState у немонтаванам компоненте і марнуючы ресурсы CPU. З ёй компонент працюе точна, його відрасункі коштаюць мало ресурсаў, і ён самастоятельна корэктуецца.

Калякі практычныя прыметкі. Браузеры зупіняюць вызывы rAF у схованых вікнах, што насправды дапамагае тут: цикл перестае касацца ресурсаў у фонавым режыме, а першая кадра пасля таго, як вікно стане видным, чытае значэнне гадзінніка і адразу пераходзіць да правильнага значэння. Порэванне на кожную кадру є незначным для аднаго таймера, але сумуецца праз дзесяткі картак.

Падход 3: абліквыція, якая ўсё у CSS

Версія з rAF все равно выкананае JavaScript на кожную кадру. Трэці падход абсалютна гэта усуне: JavaScript вырачвае адзін числовы параметр, калі компонент монтуецца, а з таго часу двіжак анімацый браузера керуе кожным цыклам.

Абсцантаванне стае анімацыяй CSS, яка практычна пераходзіць з аркуша ў аркуш, пачынаючыся з негатыўнага затрымкі, тады яна пачынаецца ў середзіне секвэнсы у правы час.

Адна паўза ўжо да тэмы выконвальных магчымасаў. Можна прачытаць, што анімацыі CSS завжды выконваюцца на нитку композітара і таму ніколі не можуць быць заблокаваныя. Гэта правда толькі для невялікага набору атрыбутаў, галоўная з яых — transform, opacity і дзеякі фільтры. Анімаванне зарэгістраваных спецыяльных атрыбутаў і створанага content, як гэта робіць даная тэхніка, обрабляецца дварчым механізам стылю на галоўнай нітку. Асёледны выгода ёсць іншая, але таксама значныя: нема таймероў, нема стану React і нема перрабаткі кожнага крока, а сэнсар дварчыка, які прыяўляе працэс анімацыі да часовай лініі дакумента, падтрымлівае правильнасць значэнняў.

Ёнкэ гэтага праблемы трэбуе саўместнай працы чатырох фічараў CSS. Мы розглянем іх по адной.

Шаг 1: адзначыць цэлыя числы, якія можна анімаваць, за дапамою @property

Вочыма бяжыць спроба анімаваць прызначэнне content безпасова. Гэта не ўспыхвае, таму што content нельга інтерполаваць: браузер не мае уявлення пра тое, як строка „59“ ператварыцца ў „58“. Вам патрэбна цифра, якую браузер можа анімаваць, і зарэгістраваная спецыяльная прызначэння даўнае гэта.

@property --s {
  syntax: '<integer>';
  inherits: true;
  initial-value: 59;
}

@property рэгіструе --s разам з паводлом. Заявленне syntax: '<integer>' паведамляе браузеру, што значэнне ўсё-такі яўляе сабою справжній цэлык, а не незрозумелы токен, што дазволяе яго інтэрполавацыю і, такім чынам, анімацыю. Установка inherits: true дазволяе наследнікам, укладаючы псевдо-элементы ::after, чытаць анімаванае значэнне з іх клёна. Это означае, што адна анімацыя на контейнеры таймера прабуджвае кожную цифру ў яму. Чырпнуць абшырэйшую інфармацыю пра зарэгістраваныя атрыбуты і пра тое, якія ўжо вони не аднацца з звычайнымі спецыяльнымі атрыбутамі, можна ў нашай інструкцыі CSS спецыяльныя атрыбуты ў часе выконання і @property.

Кожная единіца мае свой зарэгістраваны атрыбут. Гадзіны пачынаюцца з 99, і гэта становіць верхнюю межу таго, што можа паказваць гэты таймер.

@property --s {
  syntax: '<integer>';
  inherits: true;
  initial-value: 59;
} /* seconds */
@property --m {
  syntax: '<integer>';
  inherits: true;
  initial-value: 59;
} /* minutes */
@property --h {
  syntax: '<integer>';
  inherits: true;
  initial-value: 99;
} /* hours   */

Шаг 2: адаптаваць цэлычасловы значэння ў текст з дапамогою counter()

Цэлычасловае значэння все ўсё трэба ператворыць у текст з заполненням нулями (05, а не 5). Функцыя counter() адразу відображае значэння лічыльніка ў атрыбуте content, а яе вбудованы стыль decimal-leading-zero без дапамогі іншых засобаў забезпечвае заполнення двумя цифрамі.

Спосаб пераходу ад спецыяльнай властивасці да лічыльніка — це counter-reset: ён задае іменаваны лічыльнік на текущую значэння властивасці, а counter() зноў яго чытае. Хоць у вачох автара цэй фрагмент ёсць CSS, на самай праце ён выконваецца як CSS.

.seconds-digit::after {
  counter-reset: sc var(--s); /* set the "sc" counter = current --s value */
  content: counter(sc, decimal-leading-zero); /* render as "00"–"59" */
}
.minutes-digit::after {
  counter-reset: mc var(--m);
  content: counter(mc, decimal-leading-zero);
}
.hours-digit::after {
  counter-reset: hc var(--h);
  content: counter(hc, decimal-leading-zero);
}

Кожны раз, калі --s зменшваецца або збільшваецца на нова цэлычасловая значэння, двіжок стыля перарабатвае counter-reset, і атрыбут content псевдо-элемента адразу апдейтуецца. У гэтым процесе не беруць участі жадныя скрыпты.

Шаг 3: змусіць значэння зменшвацца/збільшвацца за дапамогою steps()

Па ўзначальненні анімазыя паступова пераходзіць между кантрольнымі кадрамі. Гадзіннік не павінен плаваць; ён должен рэзкая пераходзіць з адной цифры на іншую. Функцыя калібравання steps(N, end) дзеліць трываласць на N роўных, дискретных пераходаў.

@keyframes tick-s {
  from {
    --s: 59;
  }
  to {
    --s: -1;
  }
}
animation: tick-s 60s steps(60, end) infinite;

Шасцьдзесят пераходоў за шасцьдзесят секунд трываласці значыць адны пераход за секунду: 59 на пачатку, 58 пасля адной секунды, 0 пасля 59 секунд, і знову 59 калі пачынаецца наступная ітерацыя.

Ключавое слова end (эквівалентна jump-end) размешчае кожны пераход у канцы свага інтэрвалу. Роўналькава у момент t = 0 анімазыя яшчэ паказвае свяю from значэнне, таму што першы пераход яшчэ не адбыўся. Што мае значэнне, калі з’яўляюцца ад’ёмныя затрымкі: затрымка, якая заканчываецца роўналькава у момент t = 0, паказвае 59 замест 58.

Заканчэнне кія-фрамаў на -1 выглядае дзівна, але гэта намеральна. Анімацыя інтэрполюецца прыбліжна за 60 цэлах, і заводзка jump-end прыводзіць яе знову да from пры тым, калі фінальная значэнь не пазірае. Таму видныя крокі складаюцца роўна з 59, ад 0 да нуля, па чыстым кроках у адзін.

Кожная единіца мае свой кія-фрам і колькасць крокаў. Мінуты складаюцца з 60 крокаў по 60 секунд кожны, а гадзінкі — з 100 крокаў по адна гадзінка кожны:

@keyframes tick-s {
  from {
    --s: 59;
  }
  to {
    --s: -1;
  }
} /* 60 steps × 1s  = 60s  */
@keyframes tick-m {
  from {
    --m: 59;
  }
  to {
    --m: -1;
  }
} /* 60 steps × 60s = 1hr  */
@keyframes tick-h {
  from {
    --h: 99;
  }
  to {
    --h: -1;
  }
} /* 100 steps × 1hr = 100hr */

Крок 4: пачатак анімацыі ў середзіне последовасці з ад’ёмным затрымкам

Гэта центральны трюк. Калі animation-delay ў ад’ёмным значэнні, браузер спрыявае таму, што анімацыя пачалася на столькі секунд раней, таму яна пачынаецца там, дзе б ўжо дасягла за гэты час.

Скажым, у ныяндзевайшай гадзіне застаецца 23 секунды. Анімация секундаў пачынаецца з 59 і ўжо должна быць зменшылася на 36 разоў, каб паказаць 23, таму затрымка -36s ставяе яе на правыя значэння. У загальным выглядзе:

delay = -(59 - s)      where s = remaining seconds within the current minute

Ёжы прыкласты гэты прыем да ўсіх трох падазелаў часу, спачатку трэба вылічыць аб’ём застаючагу часу ў секундах, на адной з пазначак заканчэння:

const remaining = endTime - Math.floor(Date.now() / 1000);

Потым гэты аб’ём дзеліцца на часткі для кожнага падазела. Гадзіны обмежваюцца 99, таму што анімация гадзінаў пакрывае толькі 100 крокаў:

const s = remaining % 60;
const m = Math.floor(remaining / 60) % 60;
const h = Math.min(Math.floor(remaining / 3600), 99);

Памятаце пра гэтыя обмежэння як пра адмет кантролі. Ваучэр, які выгарывае за пяць дзён, паказаў бы 99:59:59 і застаўся бы на тым стане, пакуль не застанется меней чым 100 гадзін. Якщо вашы акцыі можаць трываць так дзяўно, дадзіце падазел часу «дні» або змяніце формат.

Синхронізацыя цифраў за дапамойкай адной затрымкі

Зрозумілым наступным крокам є вычысленне окремага затрымкі на кожную единіцу. Гэта прыводзіць да серйознага бага: секунды пераходзяць на 59, тады калі хвілі застаюцца на тым жа значэнні працоўна дазго.

Чаму окремыя затрымкі адхіляюцца

Возьмем remaining = 125, што адпавядае 0 гадзін, 2 хвілінам і 5 секундам. Вычысленне затрымкі секунд і хвілін незалежна дае:

delayS = -(59 - 5)       = -54s     → seconds virtual start: now - 54s
delayM = -(59 - 2) × 60  = -3420s   → minutes virtual start: now - 3420s

Кожная анімацыя вимервае сваіі перыяды руху стосова да прыватнага віртуальнага часу пачатку. Секундныя крокі выконваюцца кожную секунду, пачынаючы з now - 54s; хвілінныя крокі выконваюцца кожных 60 секунд, пачынаючы з now - 3420s. Усёлькі таму, што пункты пачатку разныя, моменты пераходу на новы значэння падпадаюць у несумяжныя моменты звычайнага гадоўніка. Цифра секунд досягае свага наступнага пераходу тут:

now - 54 + 60 = now + 6s

Аднак цифра хвілін не змěняецца па-спраба до:

now - 3420 + (58 × 60) = now - 3420 + 3480 = now + 60s

Такім чынам, за шасць секунд праз тэпер зноў пачынаюцца секунды, а за цэлую мінуту — мінуты. Пры 54 секундзях на даскане паказваецца 01:59, хоць рэальная вялічына ўсё ж меньшая за 01:00. Час паказваецца некоректна пра большую частку кожной мінуты.

Адна спяльная вачэйка

Рашэнням є наявнасць адной спяльной вачэйкі для всіх трох анімацыяў, якая досягаецца паўнаважанням афсэтов у ўжо один затрымкі:

const delay = -((99 - h) * 3600 + (59 - m) * 60 + (59 - s));
//           = -(  99×3600      +   57×60       +   54   )
//           = -(356400 + 3420 + 54)
//           = -359874s

Тепер кожная анімацыя вимерваецца з момента now - 359874с. Усі межы ўзлётаў анімацыі являюцься калькуючыміся чыслаў сваей трываласці крока, якая лічыцца ад гэтага пачатковага момента. Пагляньце на прыклад: у 60-секундным періяде анімацыя секундаў знаходзится на позыцыі 54, таму следующая межа наступае чераз 6 секунд. У 3,600-секундным періяде анімацыя хвілін знаходзится на позыцыі 3,474, а следующая межа — на пазыцыі 3,480, таксама чераз 6 секунд. Обе цифры зміняюцца адночасова у момент now + 6с і падтрымліваюць сабе альянс кожныя 60 секунд пасля гэтага.

Як адна цифра кодуе тры знакі

Сумарны затрымкі функцыонуюць як числа з сумешанай системай лічбы, падобна да таго, як HH:MM:SS адбивае тры поля ў адной строце. Кодаванне выконваецца на адной з трох складовых:

|delay| = (99 - h) × 3600    ← hours field,   each unit = 3600s
        + (59 - m) × 60      ← minutes field, each unit = 60s
        + (59 - s)           ← seconds field, each unit = 1s

Декодаванне — гэта тое, што кожная анімацыя выпраўдвае неявна, проста аналізуючы свой сабэйны періяд:

seconds position = |delay| mod 60                 → display 59 - position
minutes position = floor((|delay| mod 3600) / 60) → display 59 - position
hours   position = floor(|delay| / 3600)          → display 99 - position

Адзіёванне прыкладу праз дэкодар падтверджае гэта. 359,874 дзеліліце на 60 дае 54, таму секунды паказваюць 59 − 54 = 5. 359,874 дзеліліце на 3,600 дае 3,474; яго дзеліліце на 60 з абяроўкай дае 57, таму хвілінкі паказваюць 59 − 57 = 2. 359,874 дзеліліце на 3,600 з абяроўкай дае 99, таму гадзіны паказваюць 99 − 99 = 0. На экране паказваецца 00:02:05, што ўсуне.

Анімазіі не можаць пераскакваць адна ў адну, таму што кожны пункт дадае точна калькюляваны колькісны паказчык циклу наступнага пункта: частка гадзінаў завжды ёсць калькюляваным колькісным паказчыкам 3,600, а частка хвілінакоў — калькюляваным колькісным паказчыкам 60, таму ніякая з іх не парадзейвае наступным пунктам.

Чыстае зупіненне на 00:00:00

Анімазіі павторэнняў выкарыстоўваюць параметр infinite, таму яны самі па сабе ніколі не зупіняюцца. Калі цыкл секунд заканчваецца, ён пераходзіць на 59, і таймер, які дасягне нуля, знову прыемна пачнёць аблікаванне.

Рашэнь — это ўсё тая ж анімацыя, якая запускаецца ўсьмо раз, ровна ў момент заканчэння таймау, і прызначае ўсі тры значэння стацыярнымі нулямі. Хоць у іншым месцах яго называюць інач, гэты блак знаходзіцца ў CSS:

@keyframes freeze {
  to {
    --s: 0;
    --m: 0;
    --h: 0;
  }
}
.timer {
  animation-name: tick-s, tick-m, tick-h, freeze;
  animation-duration: 60s, 3600s, 360000s, 1ms;
  animation-timing-function: steps(60, end), steps(60, end), steps(100, end),
    step-start;
  animation-iteration-count: infinite, infinite, infinite, 1;
  animation-fill-mode: none, none, none, forwards;
}

JavaScript прызначае аднаковы таймау для трох парадоксальных анімацый, а залишчык часу як паазитывуючы таймау — для анімацыйы з замарозкай:

el.style.animationDelay = `${delay}s, ${delay}s, ${delay}s, ${remaining}s`;

Тут працюе тры элементы разам:

  • step-start (тое ж, што steps(1, jump-start)) пераходзіць да фінальнага кейфрэйма як толькі анімацыя пачынаецца, а не пасля 1 мс, таму ўсі тры значэння стаюць 0 у момент заканчэння таймау.
  • animation-fill-mode: forwards заставляе фінальны кейфрэйм застацца пасля завершэння анімацыйы, таму нулі застаюцца незменнымі.
  • Порядак у animation-name мае значэнне: калі кальколька анімацыяў задаюць тую ж власнасц, перамагае тая, якая згадана пасля. Паколькі freeze знаходзится апошнім, яго значэнне нуль перакрывае анімацыі, якія продовжаюць відбывацца пад спаднім режымам без видных наследкав.
  • Якщо купон уже выйшаў з часу дзейснення пад час завантажэння стороніцы, залишчыся час складае 0, затрымка фіксацыі стану — 0s, і таймер адразу паказвае нулі.

    Што коштае вас падход з CSS

    Дизайн без JavaScript мае рэальныя абмежэння, і яны павінны вплыць на вашае рашэнне:

    • Няма механізму завершэння таймера. Не існуе функцыі-вызова, запуску адбудовы чы змены стану, калі таймер досягае нуля. Якщо завершэння таймера павінна спрычыніць вызов API, спрятайце карточку чы адобразіце ўсё іншае — тады вам патрэбен окольны одноразовы setTimeout, які часткова вярнуе простатацю.
    • Абсалютна стылізацыя. Функцыя counter() выдае чысты текст. Шрыфт, кольор і макет належнаць усё це вашай контролі, але ўсё, што залежыць ад тэчасовага значэння ў JavaScript, напрыклад, змена кольору на адны чырвоны за 60 секунд або дадзенне класа праз певны порог, выклікае дадатковую працю.
    • Точнасць па цэлыя секунды. Залишчыцца час пад час стварэння элемента зафіксуецца на цэлыя секунды, таму візуальныя пазначкі можаць адхіляцца на даўжыну до секунды ад рэальных меж секунд. Гэта мае значэнне толькі для рекламных зворачных лічыльнікаў, але не для гадзінніка з высокай точнасцю.
    • Падтрымка браузераў. Штатак спынаецца на викорыстоўванні @property і анімаваных зарэгістраваных атрыбутаў; перад тым, як павергнуцца на ёго, пераканайцеся, чы роўна вашы браузеры падтрымліваюць гэтыя функцыі, паглядзевшы на актуальныя даны пра сумеснасць.

    У зворатнай паўзе, калі трэба толькі стварыць зворотны лічылак, які працюе толькі у режыме чытання, гэта ўзьмаўшаеся рашэнне: адна скупка анімацый на кожную картку, жадна JavaScript-коду на кожны крок лічылка, жадных адхыленняў, і няма чаго чыстаць пасля завершэння работы. Гэта выглядае чыста, калі сторанка адразу відображае сотні картак з ваучэрамі.

    Компонент React для CSS-таймера

    Версія на CSS зменяе форму компонента. Няма стану, няма цыклу анімацый і няма неабходнасці чысткі. Компонент адразу чытае данні з гадзінніка, запісвае адна властыбет стылю і відображае маркап. Ёму трэбны толькі два хукі з React:

    import { useEffect, useRef } from 'react';
    

    Компонент нижэй вычысляе застаўшыяся секунды, дзеліць іх на елементы, стварае спяльны затрымкі і запісвае чатыры такія затрымкі ў элемент. Атрыбут type толькі зміняе візуальны стыль цифр.

    interface CSSCountdownTimerProps {
      endTime: number; // Unix timestamp in seconds, same as existing prop
      type?: 'solid' | 'regular';
    }export function CSSCountdownTimer({
      endTime,
      type = 'solid',
    }: CSSCountdownTimerProps) {
      const timerRef = useRef<HTMLDivElement>(null);  useEffect(() => {
        const el = timerRef.current;
        if (!el) return;    const now = Math.floor(Date.now() / 1000);
        const remaining = Math.max(0, endTime - now);    const s = remaining % 60;
        const m = Math.floor(remaining / 60) % 60;
        const h = Math.min(Math.floor(remaining / 3600), 99);    const delay = -((99 - h) * 3600 + (59 - m) * 60 + (59 - s));    el.style.animationDelay = `${delay}s, ${delay}s, ${delay}s, ${remaining}s`;    // No cleanup needed.
        // There are no JS timers, no rAF loops, no subscriptions.
        // CSS animations stop automatically when the element is removed from the DOM.
      }, [endTime]);  return (
        <div ref={timerRef} className={`timer ${type}`}>
          <span
            className={type === 'solid' ? 'digit hours-digit' : 'hours-digit'}
          />
          <span className="sep">:</span>
          <span
            className={type === 'solid' ? 'digit minutes-digit' : 'minutes-digit'}
          />
          <span className="sep">:</span>
          <span
            className={type === 'solid' ? 'digit seconds-digit' : 'seconds-digit'}
          />
        </div>
      );
    }
    

    Як зазначаюцца ў прымэтках, няма патрэбы ў чысткі: калі элемент выходзіць з DOM, яго анімазіі таксама зникаюць. Варта рассмотрзець два удосконаленні. Першае — адтуды, што useEffect выканана пасля таго, як браузер намалюе елемент, першы кадр можа на короткі час паказаць анімазіі ў ўсходзячых позыцыях прычынай затрымкі; useLayoutEffect або вычысленне затрымкі пад час відрэндароўвання і ўведэнне яе як лінейнага стылю запобегаюць такой моменту. Другае — эфект залежыць ад endTime, таму якщо змяніцца проп, затрымкі будуць перысчысленыя, і анімазіі правільна перапозыціююцца.

    Выбор между трыма падходамі

    Простая правіла прыняцтва рашэння падходзіць для большасці ситуацыяў:

    • Якщо пасля выгорэння таймера неабяжнае якась дзейсціба, напрыклад апдэйт стану, павторны запыт дадзеных або вызов API, выкорыстаце requestAnimationFrame з перасчытам на адвесе часу. Це дае вам паўны контроль і якраз падходячую адзінку для функцыі-выкліка пасля выгорэння таймера.
    • Якщо на старонцы ёсць многа таймераў, а ніхто з іх не запускае логіку, выкорыстаце тэхніку CSS і парадзіце браузеру выканаць гэту роботу.
    • Якщо вы викорыстоўваете звычную ланцюговую структуру setTimeout, якая зменшае лічылку, заменіце яе. Цяжка ўсё правільна, і ёй яшчэ трэба выканаць роботу на галоўнай вясі кожную секунду, таму ёй прыналежаюць недзеянні обох альтэрнатыў. Бны таксама можна спяўваць: CSS для адображэння, адны спаканы таймер для логіки выгорэння.

    Галоўныя выводы

    • Задзейсненне таймера адбываецца пасля мінімальнага часу; застосавайце Date.now() пад кожным апдэйтом, ўзяўшы за базу залишчыцца часу, і тады не будзе адхыленняў.
  • За дапамою rAF трэба зберагчыць апошніяе секунды выканання ў ref і анулюваць кадр праз зняць яго.
  • Версія на CSS апоўнюецца чатырма функцыямі: цэлымі значэннямі @property, функцыяй counter() для заполнення тэксту, функцыяй steps() для дзельных пераходаў і ад’ёмным затрымкам, каб пачаць выкананне середінай секвэнцы.
  • Трэба выкарыстоўваць адну затрымку для всіх елементаў, каб цыфры пераходзілі разам, і дадаць анімацыю freeze, каб застаўліся нулі.