Как сделать таймер react

Добавил пользователь Владимир З.
Обновлено: 10.09.2024

Метод setTimeout — это встроенная функция JavaScript, устанавливающая таймер обратного отсчета (в миллисекундах) для выполнения функции обратного вызова по завершении заданного времени.

Представьте, что у вас появилась задача сделать всплывающее окно с приветствием, отображаемое через две секунды после того, как пользователь зашел на сайт. Или же необходимо воспроизвести звук через секунду после объявления победителя в онлайн-игре. В такой ситуации вам не нужно, чтобы функция запускалась немедленно. Надо отложить ее выполнение или запустить через определенный интервал времени.

Как реализовать такую задумку? Именно для решения подобных задач в JavaScript существуют два метода объекта window: setTimeout и setInterval . В этой статье мы рассмотрим их грамотное использование и практическую реализацию.

Метод setTimeout

Метод setTimeout запускается только один раз за вызов, а значит после завершения выполнения функции setTimeout прекращает свою работу.

Основной и наиболее часто используемый синтаксис этой функции:

setTimeout (функция обратного вызова, задержка в миллисекундах)

В этом примере все просто: функция обратного вызова сработает по завершении временной задержки. Давайте рассмотрим пример, как использовать эту функцию на практике:

setTimeout () позволяет нам назначить столько аргументов, сколько необходимо для функции обратного вызова. Предположим, что мы хотим поприветствовать Джона — пользователя, вошедшего в нашу систему. Для этого нам необходимо просто добавить аргументы в конец списка параметров setTimeout :

setTimeout (функция обратного вызова, задержка в миллисекундах, arg1)

Посмотрим, как это реализовать:

Важно: не передавайте аргументы функции обратного вызова напрямую и не ставьте скобки после имени функции внутри метода setTimeout , если аргументов нет!

Глядя на код предыдущего примера у вас может возникнуть соблазн передать аргументы в качестве параметров функции обратного вызова. Например:

SetTimeout (Greet(loggedInUser), 2000);

Это логическая ошибка. В приведенной выше строке кода функция обратного вызова выполняется немедленно, а параметр задержки уже не воспринимается интерпретатором. Получается, что мы не передаем объект функции в качестве метода обратного вызова в setTimeout () , а скорее устанавливаем возвращаемое значение функции. В такой ситуации таймер будет проигнорирован.

Функция clearTimeout

Иногда бывают ситуации, когда нам нужно отменить начавшуюся временную задержку. Что делать в этом случае? Метод setTimeout () возвращает ключ, позволяющий ссылаться на выполнение callback-функции. Мы можем использовать этот ключ как кнопку прерывания, тем самым взяв в свои руки контроль над самим механизмом тайм-аута. Например:

const timerId = setTimeout(Greet, 5000, loggedInUser);

Здесь timerId — тот самый ключ, используемый для ссылки на текущий таймер. Чтобы отменить тайм-аут, этот ключ нужно передать функции clearTimeout () в качестве параметра:

При использовании clearTimeout() происходит отмена таймера, и функция обратного вызова внутри метода не сработает.

Метод setInterval

Метод setInterval используется для повторного выполнения функции по истечении определенного периода времени. Схема построения этого метода следующая:

const timerID = setInterval(function, delay, arg1, arg2, . )

В этом случае задержка — это время в миллисекундах, в течение которого таймер должен задерживать последовательные выполнения функции.

Синтаксис этого метода очень похож на синтаксис setTimeout , но в отличие от последнего, setInterval запускает функцию несколько раз, пока она не будет отменена. Например:

В примере метод будет повторно выводить в консоль Hello John каждые три секунды.

Чтобы остановить непрерывный вызов функции setInterval , используется метод clearInterval() . Он принимает идентификатор таймера в качестве аргумента и использует этот идентификатор для остановки таймера.

Вернемся к нашему примеру:

Функция sayHello будет выполнена всего три раза.

Вы можете задаться вопросом, почему оператор if находится внутри функции, а не за ее пределами, вот так:

Для того чтобы ответить на него, необходимо разобраться, как JavaScript выполняет рассматриваемые нами методы. В отличие от других языков, в JS — один поток для решения задач, выполняемый построчно. Это значит, что каждая строка кода должна завершить свое выполнение, прежде чем переходить к следующей. Другими словами, выполнение остального кода блокируется.

Но есть операции ввода-вывода, которые не блокируются. Они обрабатываются базовым механизмом. К ним относятся:

  • получение данных через Ajax;
  • метод setTimeout ;
  • метод setInterval .

Поэтому JavaScript не ждет, пока функция обратного вызова, переданная методу setTimeout или setInterval , завершит выполнение прежде, чем перейти к следующей задаче или строке кода.

Следовательно, если бы мы прописали реализацию метода вторым способом, таймер не перестал бы работать, ведь после выполнения строки let timerID = setInterval(sayHello, 3000, "John") JavaScript перешел бы к следующему блоку кода if (counter === 3) и условие не выполнилось бы.

Рекурсивный setTimeout

Метод setInterval запускает функцию каждые n миллисекунд, не обращая внимания на то, когда она завершится.

Если функция каждый раз срабатывает с одним и тем же временным интервалом, тогда все в порядке:

Один и тот же интервал

Функция срабатывает с одним и тем же временным интервалом

Но бывают случаи, когда время выполнения меняется в зависимости от условий сети, например:


Время выполнения может меняться

Или перекрывает следующее:

Время выполнения перекрывает следующее за ним

Время выполнения перекрывает следующее за ним

Чтобы избежать последствий подобных случаев, вы можете запланировать рекурсивный вызов setTimeout по завершении функции обратного вызова.

Для наглядности рассмотрим распространенный пример сервиса по отправке запросов на сервер, который при перегрузке последнего увеличивал бы интервал следующего запроса:

C рекурсивным setTimeout мы можем изменять последующий вызов, исходя из результата предыдущего.

Схема здесь такая:

Фиксированная временная задержка

Фиксированная временная задержка

Другими словами, такой подход дает фиксированную временную задержку, ведь новый вызов планируется, когда закончился вызов перед ним.

Что надо помнить

На функцию из рассматриваемых методов создается внутренняя ссылка и прописывается в планировщик. Сохранившись там, она не попадет в Garbage Collector даже в том случае, когда на нее ничто не ссылается. Функцию же setInterval сотрет из памяти только метод clearInterval .

Но здесь есть свои подводные камни. Функция обратного вызова все равно будет взаимодействовать с внешними переменными, иногда занимающими больше памяти, чем она сама. В таком случае при отсутствии большой необходимости в частом вызове функции есть смысл его отменить.

Нулевая задержка

Это связано с особенностью некоторых браузеров запускать таймер на неактивной странице. Этим грешат Google Chrome, Firefox, IE10. На частоту запуска функции также влияют загрузка процессора и режим экономии энергии разряженной батареи при работе на ноутбуке.

Пример использования метода

Примечание: если методу clearTimeout передан неправильный идентификатор, то изменений не произойдет. Поэтому здесь можно не проверять работу таймера и содержимое timerId .

На этой странице представлена концепция состояния и жизненного цикла в компоненте React. Здесь вы можете найти подробный справочник API компонента.

Рассмотрим пример тикающих часов из одного из предыдущих разделов. В разделе Отрисовка элементов мы изучили только один способ обновления пользовательского интерфейса (UI). Мы вызываем ReactDOM.render() для изменения отрисованного вывода:

В этом разделе мы узнаем, как сделать компонент Clock действительно повторно используемым и инкапсулированным. Его можно будет настроить и он будет обновлять самого себя каждую секунду.

Мы можем начать с инкапсуляции кода в функциональный компонент часов:

Тем не менее, следующий код упускает ключевое требование: то, что Clock — настраиваемый таймер, который обновляет свой интерфейс каждую секунду, должно быть деталью реализации Clock .

В идеале мы хотим написать это один раз и иметь само обновление Clock :

Состояние похоже на свойство, но оно является закрытым и полностью контролируется компонентом.

Мы упоминали ранее, что компоненты, определённые как классы, имеют некоторые дополнительные возможности. Локальное состояние — это как раз одно из них: эта возможность доступна только классам.

Преобразование функции в класс

Преобразовать функциональный компонент, такой как Clock , в классовый компонент можно за пять шагов:

Добавить к нему пустой метод render() .

Перенести тело функции в метод render() .

Заменить props на this.props в теле render() .

Удалить оставшиеся пустое объявление функции.

Clock теперь определён как класс, а не функция.

Метод render будет вызываться каждый раз, когда происходит обновление, но пока мы отрисовываем в один и тот же DOM-узел, только один экземпляр класса Clock будет использоваться. Это позволяет использовать дополнительные возможности, такие как локальное состояние и хуки жизненного цикла.

Добавление локального состояния в класс

Мы переместим date из свойств в состояние за три шага:

  1. Заменить this.props.date на this.state.date в методе render() :
  1. Добавить конструктор класса, который устанавливает начальное состояние в this.state :

Обратите внимание, что мы передаём props базовому (родительскому) конструктору:

Классовые компоненты всегда должны вызывать базовый конструктор с передачей ему props .

Позже мы добавим код таймера обратно к самому компоненту.

Результат выглядит следующим образом:

Затем мы позволим настроить Clock собственным таймером с обновлением каждую секунду.

Добавление методов жизненного цикла в класс

В приложениях с множеством используемых компонентов очень важно освобождать ресурсы, занятые при их удалении.

Мы можем объявить специальные методы в классе-компоненте для выполнения кода, когда компонент устанавливается и удаляется:

Эти методы называются “хуками (методами) жизненного цикла”.

Хук componentDidMount() запускается после того, как вывод компонента отрисован в DOM. Это хорошее место для установки таймера:

Обратите внимание, что мы сохраняем идентификатор таймера в this .

Хотя this.props настраивается самим React, и у this.state есть специальное значение, вы можете добавлять дополнительные поля в класс вручную, если вам нужно сохранить что-то, что не участвует в при выводе данных (например, идентификатор таймера).

Мы удалим таймер в хуке жизненного цикла componentWillUnmount() :

Наконец, реализуем метод tick() , который компонент Clock будет запускать каждую секунду.

Он будет использовать this.setState() для планирования обновлений локального состояния компонента:

Теперь часы тикают каждую секунду.

Давайте быстро повторим, что происходит, а также перечислим порядок, в котором вызываются методы:

Когда передаётся ReactDOM.render() , React вызывает конструктор компонента Clock . Так как Clock должен отображать текущее время, он инициализирует this.state с объектом, включающим текущее время. Позднее мы обновим это состояние.

Затем React вызывает метод render() компонента Clock . Вот как React узнаёт, что должно отображаться на экране. Потом React обновляет DOM, чтобы он соответствовал выводу отрисовки Clock .

Когда в DOM вставлен вывод Clock , React вызывает хук жизненного цикла componentDidMount() . Внутри него компонент Clock указывает браузеру настроить таймер для вызова метода tick() компонента один раз в секунду.

Каждую секунду браузер вызывает метод tick() . Внутри него компонент Clock планирует обновление пользовательского интерфейса, вызывая setState() с объектом, содержащим текущее время. Благодаря вызову setState() React знает, что состояние изменилось, и снова вызывает метод render() , чтобы узнать, что должно отображаться на экране. На этот раз this.state.date в методе render() будет другим, и поэтому вывод отрисованного компонента будет включать обновлённое время. React обновляет DOM соответствующим образом.

Если компонент Clock когда-либо удаляется из DOM, React вызывает хук жизненного цикла componentWillUnmount() , чтобы оставить таймер.

Правильное использование состояния

Есть три детали о setState() , про которые нужно знать.

Не изменяйте напрямую состояние

Например, это не приведёт к повторной отрисовке компонента:

Вместо этого используйте setState() :

Конструктор — единственное место, где вы можете присвоить что-либо this.state .

Обновления состояния могут быть асинхронными

React может выполнять несколько вызовов setState() за одно обновление для лучшей производительности.

Поскольку this.props и this.state могут обновляться асинхронно, вы не должны полагаться на их значения для вычисления следующего состояния.

Например, этот код может не обновить счётчик:

Мы использовали стрелочную функцию выше, но это также работает с обычными функциями:

Обновления состояния объединяются

Когда вы вызываете setState() , React объединяет объект, который вы предоставляете c текущим состоянием.

Например, ваше состояние может содержать несколько независимых переменных:

Затем вы можете самостоятельно их обновлять с помощью отдельных вызовов setState() :

Однонаправленный поток данных

Ни родительский, ни дочерний компоненты не могут знать, является ли какой-либо компонент с состоянием или без него, и им не важно, определен ли он как функция или класс.

Вот почему состояние часто называют локальным или инкапсулированным. Оно недоступно для любого компонента, за исключением того, который владеет и устанавливает его.

Компонент может передать своё состояние вниз по дереву компонентов в виде свойства его дочерних компонентов:

Это также работает для пользовательских компонентов:

Компонент FormattedDate получил бы date в своих свойствах и не знал бы, пришло ли оно из состояния Clock , из свойств Clock или было передано напрямую:

Если вы представляете дерево компонентов как водопад свойств, состояние каждого компонента похоже на дополнительный источник воды, который соединяется с водопадом в произвольной точке, но также течёт вниз.

Чтобы показать, что все компоненты действительно изолированы, мы можем создать компонент App , который отрисовывает три компонента :

У каждого компонента Clock есть собственное состояние таймера, которое обновляется независимо от других компонентов .

В React-приложениях, независимо от того, является ли компонент, имеющим состояние или нет, — это рассматривается как деталь реализации компонента, которая может измениться со временем. Вы можете использовать компоненты без состояния в компонентах с состоянием, и наоборот.

Я пытаюсь настроить функцию TimeOut в моем компоненте. Насколько я понимаю, просто использование setTimeout, как и для Интернета, не является правильным ответом. Это вызовет проблему синхронизации и утечки памяти.

Я читал, что существует существующий интерфейс Timers API в режиме реагирования.

Однако он не соответствует ES6, я цитирую:

Имейте в виду, что если вы используете классы ES6 для своих компонентов React, встроенного API для mixins нет. Чтобы использовать TimerMixin с классами ES6, мы рекомендуем реагировать-mixin.

Примечание: миксины в основном мертвы. Используйте это только как путь миграции для устаревшего кода. Предпочитайте компоненты высокого порядка.

Поэтому мой последний вопрос: как мы правильно используем таймеры (setTimeOut), с реакцией-native, в 2017 году?

Для этого существуют два метода:

  • setTimeout позволяет вызвать функцию один раз через определённый интервал времени.
  • setInterval позволяет вызывать функцию регулярно, повторяя вызов через определённый интервал времени.

Эти методы не являются частью спецификации JavaScript. Но большинство сред выполнения JS-кода имеют внутренний планировщик и предоставляют доступ к этим методам. В частности, они поддерживаются во всех браузерах и Node.js.

setTimeout

func|code Функция или строка кода для выполнения. Обычно это функция. По историческим причинам можно передать и строку кода, но это не рекомендуется. delay Задержка перед запуском в миллисекундах (1000 мс = 1 с). Значение по умолчанию – 0. arg1 , arg2 … Аргументы, передаваемые в функцию (не поддерживается в IE9-)

Например, данный код вызывает sayHi() спустя одну секунду:

Если первый аргумент является строкой, то JavaScript создаст из неё функцию.

Это также будет работать:

Но использование строк не рекомендуется. Вместо этого используйте функции. Например, так:

Начинающие разработчики иногда ошибаются, добавляя скобки () после функции:

Это не работает, потому что setTimeout ожидает ссылку на функцию. Здесь sayHi() запускает выполнение функции, и результат выполнения отправляется в setTimeout . В нашем случае результатом выполнения sayHi() является undefined (так как функция ничего не возвращает), поэтому ничего не планируется.

Отмена через clearTimeout

Синтаксис для отмены:

В коде ниже планируем вызов функции и затем отменяем его (просто передумали). В результате ничего не происходит:

Как мы видим из вывода alert , в браузере идентификатором таймера является число. В других средах это может быть что-то ещё. Например, Node.js возвращает объект таймера с дополнительными методами.

Повторюсь, что нет единой спецификации на эти методы, поэтому такое поведение является нормальным.

Для браузеров таймеры описаны в разделе таймеров стандарта HTML5.

setInterval

Метод setInterval имеет такой же синтаксис как setTimeout :

Все аргументы имеют такое же значение. Но отличие этого метода от setTimeout в том, что функция запускается не один раз, а периодически через указанный интервал времени.

Чтобы остановить дальнейшее выполнение функции, необходимо вызвать clearInterval(timerId) .

В большинстве браузеров, включая Chrome и Firefox, внутренний счётчик продолжает тикать во время показа alert/confirm/prompt .

Рекурсивный setTimeout

Есть два способа запускать что-то регулярно.

Один из них setInterval . Другим является рекурсивный setTimeout . Например:

Метод setTimeout выше планирует следующий вызов прямо после окончания текущего (*) .

Рекурсивный setTimeout – более гибкий метод, чем setInterval . С его помощью последующий вызов может быть задан по-разному в зависимости от результатов предыдущего.

Например, необходимо написать сервис, который отправляет запрос для получения данных на сервер каждые 5 секунд, но если сервер перегружен, то необходимо увеличить интервал запросов до 10, 20, 40 секунд… Вот псевдокод:

А если функции, которые мы планируем, ресурсоёмкие и требуют времени, то мы можем измерить время, затраченное на выполнение, и спланировать следующий вызов раньше или позже.

Рекурсивный setTimeout позволяет задать задержку между выполнениями более точно, чем setInterval .

Сравним два фрагмента кода. Первый использует setInterval :

Второй использует рекурсивный setTimeout :

Для setInterval внутренний планировщик будет выполнять func(i) каждые 100 мс:

Реальная задержка между вызовами func с помощью setInterval меньше, чем указано в коде!

Это нормально, потому что время, затраченное на выполнение func , использует часть заданного интервала времени.

Вполне возможно, что выполнение func будет дольше, чем мы ожидали, и займёт более 100 мс.

В данном случае движок ждёт окончания выполнения func и затем проверяет планировщик и, если время истекло, немедленно запускает его снова.

В крайнем случае, если функция всегда выполняется дольше, чем задержка delay , то вызовы будут выполняться без задержек вообще.

Ниже представлено изображение, показывающее процесс работы рекурсивного setTimeout :

Рекурсивный setTimeout гарантирует фиксированную задержку (здесь 100 мс).

Это потому, что новый вызов планируется в конце предыдущего.

Когда функция передаётся в setInterval/setTimeout , на неё создаётся внутренняя ссылка и сохраняется в планировщике. Это предотвращает попадание функции в сборщик мусора, даже если на неё нет других ссылок.

Для setInterval функция остаётся в памяти до тех пор, пока не будет вызван clearInterval .

Есть и побочный эффект. Функция ссылается на внешнее лексическое окружение, поэтому пока она существует, внешние переменные существуют тоже. Они могут занимать больше памяти, чем сама функция. Поэтому, если регулярный вызов функции больше не нужен, то лучше отменить его, даже если функция очень маленькая.

setTimeout с нулевой задержкой

Особый вариант использования: setTimeout(func, 0) или просто setTimeout(func) .

Это планирует вызов func настолько быстро, насколько это возможно. Но планировщик будет вызывать функцию только после завершения выполнения текущего кода.

Так вызов функции будет запланирован сразу после выполнения текущего кода.

Есть и более продвинутые случаи использования нулевой задержки в браузерах, которые мы рассмотрим в главе Событийный цикл: микрозадачи и макрозадачи.

Продемонстрируем в примере ниже, что это означает. Вызов setTimeout повторно вызывает себя через 0 мс. Каждый вызов запоминает реальное время от предыдущего вызова в массиве times . Какова реальная задержка? Посмотрим:

Первый таймер запускается сразу (как и указано в спецификации), а затем задержка вступает в игру, и мы видим 9, 15, 20, 24. .

Аналогичное происходит при использовании setInterval вместо setTimeout : setInterval(f) запускает f несколько раз с нулевой задержкой, а затем с задержкой 4+ мс.

Это ограничение существует давно, многие скрипты полагаются на него, поэтому оно сохраняется по историческим причинам.

Этого ограничения нет в серверном JavaScript. Там есть и другие способы планирования асинхронных задач. Например, setImmediate для Node.js. Так что это ограничение относится только к браузерам.

Итого

  • Методы setInterval(func, delay, . args) и setTimeout(func, delay, . args) позволяют выполнять func регулярно или только один раз после задержки delay , заданной в мс.
  • Для отмены выполнения необходимо вызвать clearInterval/clearTimeout со значением, которое возвращают методы setInterval/setTimeout .
  • Вложенный вызов setTimeout является более гибкой альтернативой setInterval . Также он позволяет более точно задать интервал между выполнениями.
  • Планирование с нулевой задержкой setTimeout(func,0) или, что то же самое, setTimeout(func) используется для вызовов, которые должны быть исполнены как можно скорее, после завершения исполнения текущего кода.
  • Браузер ограничивает 4-мя мс минимальную задержку между пятью и более вложенными вызовами setTimeout , а также для setInterval , начиная с 5-го вызова.

Обратим внимание, что все методы планирования не гарантируют точную задержку.

Например, таймер в браузере может замедляться по многим причинам:

  • Перегружен процессор.
  • Вкладка браузера в фоновом режиме.
  • Работа ноутбука от аккумулятора.

Всё это может увеличивать минимальный интервал срабатывания таймера (и минимальную задержку) до 300 или даже 1000 мс в зависимости от браузера и настроек производительности ОС.

Задачи

Вывод каждую секунду

Напишите функцию printNumbers(from, to) , которая выводит число каждую секунду, начиная от from и заканчивая to .

Читайте также: