Понимание таймеров в JavaScript. Callback-функции, setTimeout, setInterval и requestAnimationFrame
В предыдущей статье Путешествие по JavaScript таймерам в сети от Нолана Лоусона многие в сети и в офлайне высказали недопонимание того, о чём там пишется, но тема всем показалась очень интересной и занимательной. Поэтому я решил исправиться и продолжить тему более детально, собрав хороший материал по каждому таймеру, где объясняется конкретно то, как он работает.
В статье присутствует адаптированный и дополненный материал с переводами статей:
Мой Твиттер — там много из мира фронтенда, да и вообще поговорим. Подписывайтесь, будет интересно: ) ✈️
Что такое callback-функции?
Или просто колбэки. Прежде, чем вообще начинать понимать таймеры и асинхронность, нужно разобраться с callback функциями. Что же это такое?
Простое определение: колбэк это функция, которая выполнится после другой функции, завершившей своё выполнение. Следовательно, отсюда и название, ‘call back’.
Определение посложнее: В JavaScript, функции это объекты. Поэтому, функции, могут брать другие функции в виде аргументов и также могут быть возвращены другими функциями. Функции которые так делают, называются функциями высшего порядка. Любая функция, которая передается как аргумент — именуется callback-функцией.
Что-то много слов. Давайте посмотрим на примерах и разберемся поглубже.
Зачем нам вообще колбэки?
По одной простой и важной причине — JavaScript это событийно-ориентированный язык. Это говорит нам о том, что вместо ожидания ответа для последующего шага, JavaScript продолжит выполнение, следя за другими событиями (ну или ивентам, кому как удобнее). Давайте взглянем на простой пример:
Как вы и ожидали, функция first выполнится первой, а функция second выполнится второй — все это выдаст в консоль следующее:
Но что, если функция first будет содержать код, который не может быть немедленно выполнен. Для примера, API запрос, где нам нужно отправить информацию, а затем подождать ответ? Чтобы симулировать такое действие, мы применим setTimeout (дальше будет подробнее про него), который является функцией JavaScript, вызывающей другую функцию после определенного количества времени. То есть, мы задержим нашу функцию на 500 миллисекунд, чтобы симулировать API запрос. Таким образом, наш новый код будет выглядеть так:
Пока что совершенно неважно, понимаете ли вы то, как работает setTimeout() . Всё, что важно — это то, чтобы вы увидели, что мы отсрочили console.log(1) на 500 миллисекунд. И так, что случится, когда мы вызовем наши функции?
Пусть даже мы и вызываем first() первой, мы выводим в лог результат этой функции, после функции second() .
Не то чтобы JavaScript не выполняет наши функции в том порядке, в котором нам надо, просто вместо этого, JavaScript не ждал ответа от first() перед тем, чтобы идти дальше, для выполнения second() .
Так зачем вам это? А затем, что вы не можете просто вызывать одну функцию за другой и надеяться, что они выполнятся в правильном порядке. Колбэки это способ убедиться в том, что конкретный код не выполняется перед другим отрезком кода, который ещё не закончил своё выполнение.
Создаём callback
Достаточно разговоров, давайте создадим callback!
Во-первых, откройте Chrome Developer Console (Windows: Ctrl + Shift + J)(Mac: Cmd + Option + J) и введите следующий код функции:
Итак, выше мы создали функцию doHomework . Наша функция берёт одну переменную, это subject . Вызовите функцию, введя следующее в вашу консоль:
Теперь давайте добавим callback — как последний параметр в функции doHomework() , мы можем передать callback. Callback-функция теперь является вторым аргументом вызова doHomework() .
Как вы видите, введя код выше в вашу консоль, вы получите один за другим два оповещения. Первое starting homework и второе, которое последует за ним finished homework.
Но колбэкам необязательно всегда быть определенными в вызове функции. Они могут быть определены где угодно в коде. Например как тут:
Результат этого примера точно такой же, как и результат предыдущего, но настройка идёт немного по-другому. Как вы видите, мы передали функцию alertFinished как аргумент, во время вызова функции doHomework() .
Пример из реального мира
На прошлой неделе я опубликовал статью о том, как создать бота в Twitter в 38 строчек кода. Единственной причиной, по которой код в этой статье работал, был API от Twitter. Когда вы делаете запросы по API, вам нужно подождать ответа, перед тем как вы сможете как-то с ним работать и соответственно на него воздействовать. Вот то, как выглядит сам запрос.
T.get просто означает то, что мы делаем get запрос к Twitter.
У этого запроса есть три параметра: ‘search/tweets’ , которые служит маршрутом для нашего запроса, params которые являются параметрами поиска и дальше идёт анонимная функция, которая и является колбэком.
Колбэк тут крайне важен, так как нам нужно подождать ответа сервера, перед тем как идти дальше в выполнении кода. Мы понятия не имеем, будет ли наш API запрос успешным или нет, так что после отправки наших параметров к search/tweets через запрос get — мы ждём. Как только Twitter ответит, вызовется наша callback-функция. Twitter либо отправит err объект (т.е. ошибку) или объект response . В нашем колбэке мы можем применить if() , чтобы определить был ли наш запрос проведен успешно или нет, а за тем уже соответственно работать с новыми данными.
В этой теме можно было бы ещё коснуться рекурсий, но это немного другая песня, которая требует более детального понимания.
Таймеры setTimeout и setInterval
Каждый блок JavaScript кода, как правило, выполняется синхронно. Но в коробке у JavaScript уже есть нативные функции (таймеры), которые позволяют задерживать выполнение какого-либо кода.
Это setTimeout() и setInterval() . Они позволят вам запустить кусок JavaScript кода в определенный момент в будущем. Такой подход называется “отложенным вызовом”. Далее вы узнаете как работают эти два метода и увидите несколько практических примеров.
setTimeout()
Эту функцию вы видели выше, а сейчас узнаете про неё ещё детальнее. Она используется в основном в тех случаях, если вы хотите запустить вашу функцию через конкретное количество миллисекунд после вызова самого setTimeout() . Синтаксис для этого метода такой:
Тут expression в JavaScript коде запустится по прошествии миллисекунд, указанных в аргументе timeout .
setTimeout() также возвращает ID для тайм-аута, чтобы его можно было отследить. Но в основном оно используется для метода clearTimeout() , который останавливает выполнение отложенной функции. В качестве аргумента тут нужно вставить ID (название) функции.
Вот ещё один пример:
При нажатии на кнопку запускается setTimeout() метод. Выражение, запуск которого по вашему предусмотрению должен произойти с задержкой в 4000ms или 4 секунды, уже передано.
Тут стоит обратить внимание на то, что setTimeout() не останавливает выполнение дальнейшего скрипта во время периода тайм-аута. Он просто откладывает выполнение указанного блока кода на заложенное количество времени. После вызова функции setTimeout() , скрипт продолжит выполняться обычным образом, с таймером на фоне.
То, что выше — это простой пример со всем кодом для alert бокса в setTimeout вызове. На практике же, вы будете вызывать функции внутри таймеров гораздо чаще. Следующий пример даст вам лучшее понимание о вызове функций с помощью setTimeout() .
Для примера, код ниже, вызывает sayHello() через одну секунду:
Вы можете также передавать аргументы вместе с функцией, например, как тут:
Как вы видите, для setTimeout() сначала передаётся функция аргумент, затем время задержки и уже только потом аргументы для функции аргумента(пардон за каламбур).
Если первый аргумент это строка, то JavaScript может создать из неё функцию. Так что вот это тоже сработает:
Но применение такого метода не рекомендуется, лучше используйте функции, как тут:
setInterval()
Эта функция, как и предполагается из названия, в основном используется для задержки функций, которые будут выполняться снова и снова, например анимации. Функция setInterval() очень близка к setTimeout() , у них даже такой же синтаксис:
Но разница тут вот в чём. setTimeout() запускает expression только единожды, в то время, как setInterval() продолжает запускать expression на регулярной основе после заданного временного интервала, пока вы не скажете стоп.
Для того, чтобы остановить последующие вызовы в setInterval() , вам нужно вызывать clearInterval(timerId) , где timerId это имя функции setInterval .
Когда вам нужно использовать setInterval() ? Когда вам не нужно вызывать setTimeout() в конце спланированной функции. Также, во время использования setInterval() , фактически не существует задержки между одним срабатыванием настоящего выражения и последующим. А в setTimeout() существует относительно долгая задержка, во время выполнения выражения, вызова функции и выставления нового setTimeout . Так что если вам нужен обычный точный таймер и надо, чтобы что-то делалось повторно после определенного временного интервала, тогда setInterval это ваш выбор.
Итак, сейчас мы подобрались к самому интересному. А именно к requestAnimationFrame . А про него нужно рассказать максимально подробно.
requestAnimationFrame()
Если вы используете анимации в своих веб-приложениях, то вы в любом случае хотите, чтобы они выполнялись как по маслу. И самым простым способом для этого является использование requestAnimationFrame , ну или просто rAF — метода который делает это непринужденно и легко.
Использование этого метода позволяет браузеру справиться с некоторыми затруднительными задачами связанными с анимацией, например такими как управление частотой кадров.
До этого разработчики использовали setTimeout и setInterval , чтобы создавать анимации. Проблема тут была в том, что для того, чтобы анимации были плавными, браузер зачастую отрисовывал кадры быстрее, чем они могут показаться на экране. Что вело к ненужным вычислениям. Также ещё одной проблемой в использовании setInterval или setTimeout было то, что эти анимации продолжали работать, даже если страница не находилась в поле видимости пользователя.
Почему нужно использовать requestAnimationFrame?
Чем же он так хорош requestAnimationFrame ? Давайте посмотрим на некоторые вещи, которые requestAnimationFrame делает значительно лучше, чем setInterval и setTimeout .
Оптимизация браузером
Использование requestAnimationFrame даёт браузеру возможность оптимизировать анимации, чтобы делать их плавнее и более ресурсоэффективными. Не будем сильно вдаваться в детали того, как браузер это делает, просто знайте, что requestAnimationFrame исключает возможность ненужных отрисовок и может связывать вместе несколько анимаций в одно целое и цикл перерисовки.
Анимации работают, когда их видно
Используя requestAnimationFrame ваши анимации будут работать только тогда, когда вкладка со страницей видима пользователю. А это означает меньшее CPU, GPU и использование памяти, что приводит нас к последнему моменту эффективности.
Меньшее потребление питания
Оптимизации, упомянутые в предыдущих двух моментах помогают сократить количество “мусорных процессов”, которые нужно совершить устройству, чтобы создать анимацию и, следовательно, это ведет к бережному энергопотреблению. Это особенно важно для мобильных устройств, которые обычно имеют относительно короткие сроки работы батареи.
Используем requestAnimationFrame
Этому методу должна быть передана колбэк функция, которая отвечает за отрисовку одного кадра вашей анимации. Для того, чтобы создать полную анимацию, вам понадобится сделать этот колбэк рекурсивным.
Временная метка с высоким разрешением DOMHighResTimeStamp передаётся колбэку. Вам не понадобится всегда это использовать, но это может быть довольно полезным для некоторых анимаций.
Пример ниже показывает то, как настроить рекурсивную функцию, которая использует requestAnimationFrame .
Стоит упомянуть, что у вас есть только 16.67 миллисекунд, чтобы отрендерить каждый кадр. С точки зрения времени это не очень хорошо, так что вам нужно быть осторожным с тем, что вы хотите выполнить внутри колбэк функции. Если ваш кадр требует больше 16.67 секунд на обработку, то анимация может выйти не совсем плавной.
requestAnimationFrame отдаст requestID , который может быть использован для отмены запланированного кадра анимации.
Отменяем кадры анимации
Чтобы отменить кадр анимации вы можете использовать метод cancelAnimationFrame . Этот метод должен принять requestID для кадра, который вы хотите отменить.
Позже вы узнаете то, как отслеживать актуальный requestID .
Полифил
Есть великолепный полифилл для requestAnimationFrame , который разработал Эрик Мюллер из Opera и который далее доработали Пол Айриш и Тино Зийдел. Код лежит тут.
Создаём простую демку с requestAnimationFrame
После того, как вы поняли теорию по requestAnimationFrame , давайте создадим простую демку. Вот она.
Подготавливаем HTML и CSS
Откройте свой любимый текстовый редактор и создайте файл под названием index.html . После этого, добавьте следующий код в новый файл.
В этой разметке вы указываете число кнопок, которые будут использоваться для запуска, остановки и сброса анимации. Вы также определите canvas, в котором и будет происходить сама анимация.
Стоит обратить внимание, что тут есть два файла в <script>, который находится в конце этой разметки. raf-polyfill.js это полифил о котором мы говорили в предыдущей секции. Убедитесь в том, что вы скачали этот файл и сохранили его в той же директории, что и index.html . Вам также надо будет скопировать style.css отсюда в папку вашего проекта.
Настраиваем JavaScript
Теперь, как только вы разобрались с HTML и CSS, настало время писать JavaScript код, который будет обрабатывать отрисовку анимации в <canvas> . Если до этого вы не использовали Canvas API, то не беспокойтесь, я объясню всё что нужно по мере прочтения статьи.
Создайте новый файл в папке проекта под название script.js и добавьте туда этот код:
Вы создали три переменные и ассоциировали их с кнопками из разметки.
Дальше вам надо написать немного кода, чтобы настроить Canvas. Скопируйте этот код в свой script.js файл.
Тут сначала мы создаем переменную под названием сanvas и ассоциируем её с элементом из разметки. Далее вы задаёте 2d контекст отрисовки для него. Это даёт нам методы для отрисовки объектов на нём, так же как и методы контроля стилей этих объектов.
Следующая строка кода выставляет свойству fillStyle для контекста отрисовки #212121 .
requestID переменная будет использоваться, чтобы отслеживать requestID , возвращенный методом requestAnimationFrame .
Переменные posX , boxWidth и pixelPerFrame используются для выставления позиции блоку при отрисовке; ширина блока; и число пикселей на которое блок должен двигаться при каждом кадре.
Под конец вы вызываете контекст отрисовки методом fillReact , передав ему X и Y координаты положения прямоугольника вместе с его высотой и шириной.
Написание анимированной функции
Далее вам надо написать функцию animate , которая будет ответственна за прорисовку кадров.
Скопируйте следующий код в ваш script.js файл.
Вызов requestAnimateFrame вверху функции запланирует следующий кадр анимации. Он размещается сначала, так как мы можем подобраться как можно ближе к 60 FPS, при использовании setTimeout фолбэка, которое применяет полифил.
Далее у вас будет if проверка, которая проверяет достиг ли блок крайней правой стороны canvas’а. Если блок ещё этого не сделал, то вы используете clearRect метод, чтобы удалить блок, отрисованный в предыдущем кадре и затем отрисовать блок на новой позиции с использованием fillRect . Если же блок достиг конца canvas’а, то вы вызываете cancelAnimationFrame , чтобы отменить планирование кадра в начале animate функции. И под конец, вы обновляете posX переменную с позицией на которой блок должен быть отрисован при следующем кадре.
Цепляем кнопки
Последнее, что нам нужно сделать для того, чтобы демо работало — это настроить срабатывания по событиям для кнопок старта, остановки и сброса. Добавьте следующий код в ваш script.js файл.
Тут у нас три обработчика событий. Первые два запускают и останавливают анимацию, а последний запускается при нажатии на кнопку reset. Это выставит переменной posX значение 0 . Также это очистит canvas — мы не так сильно обеспокоены производительностью, так что это нормально делать таким ленивым способом — и отрисовывать блок обратно на его стартовой позиции.
Таймер обратного отсчёта на чистом JavaScript
В этой статье рассмотрим таймер обратного отсчета, построенный на чистом CSS и JavaScript. Он написан с использованием минимального количества кода без использования jQuery и каких-либо других сторонних библиотек.
Таймеры обратного отсчёта могут использоваться на сайтах для различных целей. Но в большинстве случаев они применяются для отображения времени, которое осталось до наступления какого-то крупного события: запуска нового продукта, рекламной акции, начала распродажи и т.д.
Демо таймера обратного отсчёта
Простой таймер обратного отсчета с днями, часами, минутами и секундами. Очень легко настраивается. Создан на чистом CSS и Javascript (без зависимостей).
Подключение и настройка таймера
1. Вставить в нужное место страницы html-разметку таймера:
Таймер обратного отсчета отображает четыре числа: дни, часы, минуты и секунды. Каждое число находится в элементе <div> . Первый класс ( timer__item ) используется для стилизации элемента, а второй — для таргетинга в JavaScript.
2. Добавить стили (базовое оформление):
Стилизовать таймер обратного отсчета можно так как вы этого хотите.
Вышеприведённый CSS использует flexbox. Знак «:» и текст под каждым компонентом даты выводиться на страницу с помощью псевдоэлементов.
3. Добавить JavaScript:
4. Установить дату окончания. Например, до 1 июля 2021:
Структура кода JavaScript
Основную часть кода занимает функция countdownTimer :
Эта функция выполняет расчёт оставшегося времени и обновляет содержимое элементов .timer__item на странице.
Расчёт оставшегося времени осуществляется посредством вычитания текущей даты из конечной:
Вычисление оставшегося количества дней, часов, минут и секунд выполняется следующим образом:
Встроенная функция Math.floor используется для округления числа до ближайшего целого (посредством отбрасывания дробной части).
Вывод оставшегося времени на страницу:
Переменные $days , $hours , $minutes , $seconds содержат элементы (таргеты), в которые выводятся компоненты времени.
Изменение содержимого элементов выполняется через textContent . Если значение меньше 10, то к нему добавляется символ «0».
Получение элементов (выполняется с помощью querySelector ):
Функция declensionNum используется для склонения числительных:
Для постоянного вычисления оставшегося времени и вывода его на страницу используется setInterval .
Хранение идентификатора таймера осуществляется в переменной timerId :
Использование setInterval для запуска функции countdownTimer каждую секунду:
Остановка таймера по истечении времени выполняется в функции countdownTimer :
Скрипт для создания нескольких таймеров отчета на странице
Скрипт, написанный с использованием классов, который можно использовать для создания нескольких таймеров отчета на странице:
Пример использования класса CountdownTimer() для создания таймера на странице:
В new CountdownTimer() необходимо передать следующие аргументы:
- конечную дату в формате Date;
- функцию, которую нужно выполнять каждую секунду (её, например, можно использовать для обновления содержимого элементов, которые используются для отображения оставшегося времени);
- при необходимости функцию, которую нужно выполнить после завершения таймера.
HTML код первого таймера:
Инициализация остальных таймеров на странице с помощью new CountdownTimer() выполняется аналогично.
Пример страницы, на которой имеется несколько таймеров обратного отсчёта:
Таймеры JavaScript: все что нужно знать
Здравствуйте, коллеги. Давным-давно на Хабре уже переводилась статья под авторством Джона Резига как раз на эту тему. Прошло уж 10 лет, а тема по-прежнему требует разъяснений. Поэтому предлагаем интересующимся почитать статью Самера Буны, в которой дается не только теоретический обзор таймеров в JavaScript (в контексте Node.js), но и задачи на них.
Несколько недель назад я опубликовал в Твиттере следующий вопрос с одного собеседования:
***Ответьте на него для себя, а потом читайте дальше ***
Примерно половина ответов на этот твит были неверными. Нет, дело НЕ СВЯЗАНО с V8 (или другими VM). Функции вроде setTimeout и setInterval , гордо именуемые «Таймерами JavaScript», не входят ни в одну спецификацию ECMAScript или в реализацию движка JavaScript. Функции-таймеры реализуются на уровне браузера, поэтому в разных браузерах их реализации отличаются. Также таймеры нативно реализуются в самой среде исполнения Node.js.
В браузерах основные функции-таймеры относятся к интерфейсу Window , также связанному с некоторыми другими функциями и объектами. Этот интерфейс предоставляет ко всем своим элементам глобальный доступ в главной области видимости JavaScript. Вот почему функцию setTimeout можно выполнять непосредственно в консоли браузера.
В Node таймеры входят в состав объекта global , который устроен подобно браузерному интерфейсу Window . Исходный код таймеров в Node показан здесь.
Кому-то может показаться, что это просто плохой вопрос с собеседования – какой вообще прок знать подобное?! Я, как JavaScript-разработчик, думаю так: предполагается, что вы должны это знать, поскольку обратное может свидетельствовать, что вы не вполне понимаете, как V8 (и другие виртуальные машины) взаимодействует с браузерами и Node.
Рассмотрим несколько примеров и решим парочку задач на таймеры, давайте?
Для запуска примеров из этой статьи можно воспользоваться командой node. Большинство рассмотренных здесь примеров фигурируют в моем курсе Getting Started with Node.js на Pluralsight.
Отложенное выполнение функции
Таймеры – это функции высшего порядка, при помощи которых можно откладывать или повторять выполнение других функций (таймер получает такую функцию в качестве первого аргумента).
Вот пример отложенного выполнения:
В этом примере при помощи setTimeout вывод приветственного сообщения откладывается на 4 секунды. Второй аргумент setTimeout — это задержка (в мс). Я умножаю 4 на 1000, чтобы получилось 4 секунды.
Первый аргумент setTimeout – функция, выполнение которой будет откладываться.
Если выполнить файл example1.js командой node, Node приостановится на 4 секунды, а затем выведет приветственное сообщение (после чего последует выход).
Обратите внимание: первый аргумент setTimeout — это всего лишь ссылка на функцию. Она не должна быть встроенной функцией – такой, как example1.js . Вот тот же самый пример без использования встроенной функции:
Передача аргументов
Если функция, для задержки которой используется setTimeout , принимает какие-либо аргументы, то можно использовать оставшиеся аргументы самой функции setTimeout (после тех 2, которые мы уже успели изучить) для переброски значений аргументов к отложенной функции.
Вышеприведенная функция rocks , отложенная на 2 секунды, принимает аргумент who , и вызов setTimeout передает ей значение “Node.js” в качестве такого аргумента who .
При выполнении example2.js командой node фраза “Node.js rocks” будет выведена на экран через 2 секунды.
Задача на таймеры #1
Итак, опираясь на уже изученный материал о setTimeout , выведем 2 следующих сообщения после соответствующих задержек.
- Сообщение “Hello after 4 seconds” выводим через 4 секунды.
- Сообщение “Hello after 8 seconds” выводим через 8 секунд.
В вашем решении можно определить всего одну функцию, содержащую встроенные функции. Это означает, что множество вызовов setTimeout должны будут использовать одну и ту же функцию.
Вот как я бы решил эту задачу:
У меня theOneFunc получает аргумент delay и использует значение данного аргумента delay в сообщении, выводимом на экран. Таким образом, функция может выводить разные сообщения в зависимости от того, какое значение задержки мы ей сообщим.
Затем я использовал theOneFunc в двух вызовах setTimeout , причем, первый вызов срабатывает через 4 секунды, а второй – через 8 секунд. Оба эти вызова setTimeout также получают 3-й аргумент, представляющий аргумент delay для theOneFunc .
Выполнив файл solution1.js командой node, мы выведем на экран требования задачи, причем, первое сообщение появится через 4 секунды, а второе — через 8 секунд.
Повторяем выполнение функции
А что, если бы я задал вам выводить сообщение каждые 4 секунды, неограниченно долго?
Конечно, можно заключить setTimeout в цикл, но в API таймеров также предлагается функция setInterval , при помощи которой можно запрограммировать «вечное» выполнение какой-либо операции.
Вот пример setInterval :
Этот код будет выводить сообщение каждые 3 секунды. Если выполнить example3.js командой node , то Node будет выводить эту команду до тех пор, пока вы принудительно не завершите процесс (CTRL+C).
Отмена таймеров
Поскольку при вызове функции таймера назначается действие, это действие также можно отменить, прежде, чем он будет выполнен.
Вызов setTimeout возвращает ID таймера, и можно использовать этот ID таймера при вызове clearTimeout , чтобы отменить таймер. Вот пример:
Этот простой таймер должен срабатывать через 0 мс (то есть, сразу же), но этого не произойдет, поскольку мы захватываем значение timerId и немедленно отменяем этот таймер при помощи вызова clearTimeout .
При выполнении example4.js командой node , Node ничего не напечатает — процесс просто сразу же завершится.
Кстати, в Node.js предусмотрен и другой способ задать setTimeout со значением 0 мс. В API таймеров Node.js есть еще одна функция под названием setImmediate , и она в принципе делает то же самое, что и setTimeout со значением 0 мс, но в данном случае задержку можно не указывать:
Функция setImmediate поддерживается не во всех браузерах. Не используйте ее в клиентском коде.
Наряду с clearTimeout есть функция clearInterval , которая делает то же самое, но с вызовами setInerval , а также есть вызов clearImmediate .
Задержка таймера – вещь не гарантированная
Вы заметили, что в предыдущем примере при выполнении операции с setTimeout после 0 мс эта операция происходит не сразу же (после setTimeout ), а только после того, как будет целиком выполнен весь код скрипта (в том числе, вызов clearTimeout )?
Позвольте мне пояснить этот момент на примере. Вот простой вызов setTimeout , который должен бы сработать через полсекунды — но этого не происходит:
Сразу после определения таймера в данном примере мы синхронно блокируем среду времени выполнения большим циклом for . Значение 1e10 равно 1 с 10 нулями, поэтому цикл длится 10 миллиардов процессорных тактов (в принципе, так имитируется перегруженный процессор). Node ничего не может сделать, пока этот цикл не завершится.
Разумеется, на практике так делать очень плохо, но данный пример помогает понять, что задержка setTimeout – это не гарантированное, а, скорее, минимальное значение. Величина 500 мс означает, что задержка продлится минимум 500 мс. На самом деле, скрипту потребуется гораздо больше времени для вывода приветственной строки на экран. Сначала ему придется дождаться, пока завершится блокирующий цикл.
Задача на таймеры #2
Напишите скрипт, который будет выводить сообщение “Hello World” раз в секунду, но всего 5 раз. После 5 итераций скрипт должен вывести сообщение “Done”, после чего процесс Node завершится.
Ограничение: при решении данной задачи нельзя вызывать setTimeout .
Подсказка: нужен счетчик.
Вот как я бы решил эту задачу:
В качестве исходного значения counter я задал 0, а затем вызвал setInterval , берущий его id.
Отложенная функция будет выводить сообщение и всякий раз при этом увеличивать счетчик на единицу. Внутри отложенной функции у нас инструкция if, которая будет проверять, не прошло ли уже 5 итераций. По истечении 5 итераций программа выведет “Done” и очистит значение интервала, воспользовавшись захваченной константой intervalId . Задержка интервала — 1000 мс.
«Кто» именно вызывает отложенные функции?
При использовании ключевого слова JavaScript this внутри обычной функции, вот так например:
значение в ключевом слове this будет соответствовать вызывающей стороне. Если определить вышеупомянутую функцию внутри Node REPL, то вызывать ее будет объект global . Если определить функцию в консоли браузера, то вызывать ее будет объект window .
Давайте определим функцию как свойство объекта, чтобы стало немного понятнее:
Теперь, когда при работе с функцией obj.whoCallMe мы будем напрямую использовать ссылку на нее, в качестве вызывающей стороны будет выступать объект obj (идентифицируемый по своему id ):
А теперь вопрос: кто будет вызывающей стороной, если передать ссылку на obj.whoCallMe вызову setTimetout ?
Кто в данном случае вызывающий?
Ответ будет отличаться в зависимости от того, где выполняется функция таймера. В данном случае просто недопустима зависимость от того, кто — вызывающая сторона. Вы утратите контроль над вызывающей стороной, поскольку именно от реализации таймера будет зависеть, кто в данном случае вызывает вашу функцию. Если протестировать этот код в Node REPL, то вызывающей стороной окажется объект Timeout :
Обратите внимание: это важно лишь в случае, когда ключевое слово JavaScript this используется внутри обычных функций. При использовании стрелочных функций вызывающая сторона вас вообще не должна беспокоить.
Задача на таймеры #3
Напишите скрипт, который будет непрерывно выводить сообщение “Hello World” с варьирующимися задержками. Начните с односекундной задержки, после чего на каждой итерации увеличивайте ее на секунду. На второй итерации задержка будет 2 секунды. На третьей — три, и так далее.
Включите задержку в выводимое сообщение. У вас должен получиться примерно такой вывод:
Hello World. 1
Hello World. 2
Hello World. 3
.
Ограничения: переменные можно определять только при помощи const. При помощи let или var — нельзя.
Поскольку длительность задержки в данной задаче – величина переменная, использовать setInterval здесь нельзя, но можно вручную настроить интервальное выполнение при помощи setTimeout внутри рекурсивного вызова. Первая выполненная функция с setTimeout будет создавать следующий таймер, и так далее.
Кроме того, поскольку нельзя использовать let / var , у нас не может быть счетчика для приращения задержки при каждом рекурсивном вызове; вместо этого можно воспользоваться аргументами рекурсивной функции, чтобы выполнять приращение во время рекурсивного вызова.
Вот как можно было бы решить эту задачу:
Задача на таймеры #4
Напишите скрипт, который будет выводить сообщение “Hello World” с такой же структурой задержек, как и в задаче #3, но на этот раз группами по 5 сообщений, а в группах будет основной интервал задержки. Для первой группы из 5 сообщений выбираем исходную задержку в 100 мс, для следующей – 200 мс, для третьей – 300 мс и так далее.
Вот как должен работать этот скрипт:
- На отметке 100 мс скрипт впервые выводит “Hello World”, и делает так 5 раз с интервалом, нарастающим по 100 мс. Первое сообщение появится через 100 мс, второе через 200 мс и т.д.
- После первых 5 сообщений скрипт должен увеличивать основную задержку уже на 200 мс. Таким образом, 6-е сообщение будет выведено через 500 мс + 200 мс (700 мс), 7-е — 900 мс, 8-е сообщение – через 1100 мс, и так далее.
- После 10 сообщений скрипт должен увеличивать основной интервал задержки на 300 мс. 11-е сообщение должно быть выведено через 500 мс + 1000 мс + 300 мс (18000 мс). 12-е сообщение должно быть выведено через 2100 мс, и т.д.
Включите задержку в выводимое сообщение. У вас должен получиться примерно такой вывод (без комментариев):
Hello World. 100 // При 100 мс
Hello World. 100 // При 200 мс
Hello World. 100 // При 300 мс
Hello World. 100 // При 400 мс
Hello World. 100 // При 500 мс
Hello World. 200 // При 700 мс
Hello World. 200 // При 900 мс
Hello World. 200 // При 1100 мс
.
Ограничения: Можно использовать лишь вызовы setInterval (а не setTimeout ) и только ОДНУ инструкцию if .
Поскольку мы можем работать только с вызовами setInterval , здесь нам потребуется использовать рекурсию, а также увеличивать задержку следующего вызова setInterval . Кроме того, инструкция if понадобится нам, чтобы это стало происходить лишь после 5 вызовов этой рекурсивной функции.
How to create an accurate timer in javascript?
After exactly 3600 seconds, it prints about 3500 seconds.
Why is it not accurate?
How can I create an accurate timer?
15 Answers 15
Because you are using setTimeout() or setInterval() . They cannot be trusted, there are no accuracy guarantees for them. They are allowed to lag arbitrarily, and they do not keep a constant pace but tend to drift (as you have observed).
How can I create an accurate timer?
Use the Date object instead to get the (millisecond-)accurate, current time. Then base your logic on the current time value, instead of counting how often your callback has been executed.
For a simple timer or clock, keep track of the time difference explicitly:
Now, that has the problem of possibly jumping values. When the interval lags a bit and executes your callback after 990 , 1993 , 2996 , 3999 , 5002 milliseconds, you will see the second count 0 , 1 , 2 , 3 , 5 (!). So it would be advisable to update more often, like about every 100ms, to avoid such jumps.
However, sometimes you really need a steady interval executing your callbacks without drifting. This requires a bit more advanced strategy (and code), though it pays out well (and registers less timeouts). Those are known as self-adjusting timers. Here the exact delay for each of the repeated timeouts is adapted to the actually elapsed time, compared to the expected intervals: