Объединенный асинхронный JavaScript: Таймайты и интервалы

В процессе перевода.

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

Необходимые условия: Базовая компьютерная грамотность, достаточное понимание основ JavaScript.
Цель: Понимание асинхронных циклов и интервалов, и то как их можно использовать.

Введение

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

Эти функции:

setTimeout()
Выполняет указанный блок кода один раз по истечении указанного времени
setInterval()
Выполняет указанный блок кода несколько раз с определенным интервалом между каждым вызовом.
requestAnimationFrame()
Современная версия setInterval (). Выполняут указанный блок кода перед тем, как браузер в следующий раз перерисовывает отображение, позволяя запускать анимацию с подходящей частотой кадров независимо от среды, в которой она выполняется.

Асинхронный код, установленный этими функциями, выполняется в основном потоке (по истечении указанного им таймера).

Важно знать, что вы можете (и часто будете) запускать другой код до выполнения вызова setTimeout () или между итерациями setInterval (). В зависимости от того, насколько интенсивно используются эти операции для процессора, они могут еще больше задержать выполнение асинхронного кода, поскольку любой асинхронный код будет выполняться только после того, как станет доступен основной поток. (Другими словами, когда стек пуст.) Вы узнаете больше по этому вопросу по мере изучения этой статьи.

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

setTimeout()

Как мы ранее отметили, setTimeout () выполняет определенный блок кода один раз по истечении заданного времени. Принимает следующие параметры:

  • Функция для запуска или ссылка на функцию, определенную в другом месте.
  • Число, представляющее интервал времени в миллисекундах (1000 миллисекунд равняется 1 секунде) ожидания перед выполнением кода. Если вы укажете значение 0 (или просто опустите значение), функция запустится как можно скорее. (См. Примечание ниже о том, почему он запускается «как можно скорее», а не «сразу».) Подробнее о том, почему вы, возможно, захотите сделать это позже.
  • Значений, представляющие любые параметры, которые вы хотите передать функции при ее запуске.

NOTE:  Указанное время (или задержка) не является гарантированным временем выполнения, а скорее минимальным временем выполнения. Обратные вызовы, которые вы передаете этим функциям, не могут выполняться, пока стек в основном потоке не станет пустым.

Как следствие, такой код, как setTimeout (fn, 0), будет выполняться, как только стек будет пуст, а не сразу. Если вы выполните такой код, как setTimeout (fn, 0), но сразу после выполнения цикла, который насчитывает от 1 до 10 миллиардов, ваш обратный вызов будет выполнен через несколько секунд.

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

let myGreeting = setTimeout(function() {
  alert('Hello, Mr. Universe!');
}, 2000)

Указанные вами функции не обязательно должны быть анонимными. Вы можете дать своей функции имя и даже определить ее где-нибудь еще и передать ссылку на функцию в setTimeout (). Следующие две версии фрагмента кода эквивалентны первой:

// С именованной функцией
let myGreeting = setTimeout(function sayHi() {
  alert('Hello, Mr. Universe!');
}, 2000)

// С функцией определенной отдельно
function sayHi() {
  alert('Hello Mr. Universe!');
}

let myGreeting = setTimeout(sayHi, 2000);

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

setTimeout () возвращает значение идентификатора, которое можно использовать для ссылки на тайм-аут позже, например, когда вы хотите его остановить.

Передача параметров в функцию setTimeout ()

Любые параметры, которые вы хотите передать функции, выполняемой внутри setTimeout (), должны быть переданы ей как дополнительные параметры в конце списка.

Например, вы можете реорганизовать предыдущую функцию, чтобы она передавала привет любому имени, переданному ей:

function sayHi(who) {
  alert(`Hello ${who}!`);
}

Теперь вы можете передать имя в вызов setTimeout () в качестве третьего параметра:

let myGreeting = setTimeout(sayHi, 2000, 'Mr. Universe');

Очистка таймаутов

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

clearTimeout(myGreeting);

Note: См.greeter-app.html для более полной демонстрации, которая позволяет вам указать имя для приветствия и отменить приветствие с помощью отдельной кнопки (см. исходный код).

setInterval()

setTimeout () отлично работает, когда вам нужно один раз запустить код по истечении заданного периода времени. Но что происходит, когда вам нужно запускать код снова и снова - например, в случае анимации?

Здесь пригодится setInterval() . Работает очень похоже на setTimeout (), за исключением того, что функция, которую вы передаете в качестве первого параметра, выполняется повторно не менее чем за количество миллисекунд, заданных вторым параметром. Вы также можете передать любые параметры, необходимые для выполняемой функции, в качестве последующих параметров вызова setInterval ().

Давайте посмотрим на пример. Следующая функция создает новый объект Date(), с помощью toLocaleTimeString() извлекает из него строку с временем и отображает ее в пользовательском интерфейсе. Затем он запускает функцию один раз в секунду с помощью setInterval(), создавая эффект цифровых часов, которые обновляются раз в секунду ( реальный пример, и исходный код):

function displayTime() {
   let date = new Date();
   let time = date.toLocaleTimeString();
   document.getElementById('demo').textContent = time;
}

const createClock = setInterval(displayTime, 1000);

Как и setTimeout (), setInterval () возвращает определенное значение, которое вы можете использовать позже, когда вам нужно очистить интервал.

Очистка интервала

setInterval () выполняет задачу постоянно. setInterval () продолжает выполнять задачу вечно, если вы что-то с ней не сделаете. Возможно, вам понадобится способ остановить такие задачи, иначе вы можете получить ошибки, если браузер не сможет выполнить какие-либо другие версии задачи или если анимация, обрабатываемая задачей, завершилась. Вы можете сделать это так же, как останавливаете timeouts - передавая идентификатор, возвращаемый вызовом setInterval (), в функцию clearInterval ():

const myInterval = setInterval(myFunction, 2000);

clearInterval(myInterval);

Активное обучение: Создание собственного секундомера!

Учитывая все вышесказанное, у нас есть для вас задача. Возьмите копию нашего примера setInterval-clock.html , и измените ее так, чтобы создать свой собственный простой секундомер.

Вам нужно отображать время, как и раньше, но в этом примере вам нужно:

  • Кнопка "Start" для запуска секундомера.
  • Кнопка "Stop" для паузы/остановки.
  • Кнопка "Reset", чтобы сбросить счетчик времени на 0.
  • Дисплей времени, чтобы отображать количество прошедших секунд а не фактическое время.

Несколько подсказок для вас:

  • Вы можете структурировать и стилизовать разметку кнопок по своему усмотрению; просто убедитесь, что вы используете семантический HTML с ковычками, которые позволяют захватывать ссылки на кнопки с помощью JavaScript.
  • Вероятно, вы захотите создать переменную, которая начинается с 0, а затем увеличивается на единицу каждую секунду с использованием постоянного цикла.
  • Этот пример проще создать без использования объекта Date (), как мы это делали в нашей версии, но он будет менее точен - вы не можете гарантировать, что обратный вызов сработает ровно через 1000 мс. Более точным способом было бы запустить startTime = Date.now (), чтобы получить метку времени, когда пользователь нажал кнопку запуска, а затем выполнить Date.now () - startTime, чтобы получить количество миллисекунд после того, как была нажата кнопка запуска .
  • Вам также нужно рассчитать количество часов, минут и секунд как отдельные значения, а затем отображать их вместе в строке после каждой итерации цикла. На втором счетчике вы можете отработать каждую из них.
  • Как вы могли бы их рассчитать? Подумайте об этом:
    • В одном часе 3600 секунд.
    • Количество минут - это количество секунд, оставшееся после вычитания всех часов, разделенное на 60.
    • Количество секунд будет количеством секунд, оставшихся после вычитания всех минут.
  • Вам необходимо включить начальный ноль в отображаемые значения, если сумма меньше 10, чтобы они больше походили на традиционные часы.
  • Чтобы приостановить секундомер, вам нужно очистить интервал. Чтобы сбросить его, вам нудно установить счетчик обратно на 0, очистить интервал, а затем немедленно обновить отображение.
  • Вероятно, вам следует отключить кнопку запуска после ее нажатия один раз и снова включить ее после того, как вы остановили / сбросили ее. В противном случае многократное нажатие кнопки запуска приведет к применению нескольких setInterval () к часам, что приведет к неправильному поведению.

Note: Если вы застрали, вы можете увидеть нашу версию (см. также исходный код ).

Что нужно помнить о setTimeout () и setInterval ()

При работе с setTimeout () и setInterval () следует помнить о нескольких вещах. Давайте рассмотрим их.

Рекурсивые таймауты

Есть еще один способ использования setTimeout (): вы можете вызвать его рекурсивно для повторного запуска одного и того же кода вместо использования setInterval ().

В приведенном ниже примере используется рекурсивный setTimeout () для запуска переданной функции каждые 100 миллисекунд:

let i = 1;

setTimeout(function run() {
  console.log(i);
  i++;
  setTimeout(run, 100);
}, 100);

Сравните приведенный выше пример со следующим - здесь используется setInterval () для достижения того же эффекта:

let i = 1;

setInterval(function run() {
  console.log(i);
  i++
}, 100);

Чем рекурсивный setTimeout () отличается от setInterval () ?

Разница между двумя версиями приведенного выше кода невелика.

  • Рекурсивный setTimeout () гарантирует такую же задержку между выполнениями. (Например, 100 мс в приведенном выше случае.) Код будет запущен, затем подождет 100 миллисекунд, прежде чем запустится снова, поэтому интервал будет одинаковым, независимо от того, сколько времени требуется для выполнения кода.
  • Пример с использованием setInterval () работает несколько иначе. Выбранный вами интервал включает время, затрачиваемое на выполнение кода, который вы хотите запустить. Предположим, что выполнение кода занимает 40 миллисекунд - тогда интервал составляет всего 60 миллисекунд.
  • При рекурсивном использовании setTimeout () каждая итерация может вычислять различную задержку перед запуском следующей итерации. Другими словами, значение второго параметра может указывать другое время в миллисекундах для ожидания перед повторным запуском кода.

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

Немедленные таймауты

Использование 0 в качестве значения для setTimeout () позволяет планировать выполнение указанной функции обратного вызова как можно скорее, но только после того, как будет запущен основной поток кода.

Например, код приведенный ниже (рабочий код) выводит alert содержащий "Hello", затем alert содержащий "World" как только вы нажмете ОК в первом alert.

setTimeout(function() {
  alert('World');
}, 0);

alert('Hello');

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

Очистка с помощью clearTimeout() или clearInterval()

clearTimeout () и clearInterval () используют один и тот же список записей для очистки. Интересно, что это означает, что вы можете использовать любой метод для очистки setTimeout () или setInterval ().

Для согласованности следует использовать clearTimeout () для очистки записей setTimeout () и clearInterval () для очистки записей setInterval (). Это поможет избежать путаницы.

requestAnimationFrame()

requestAnimationFrame() это специализированная функция цикла, созданная для эффективного запуска анимации в браузере. По сути, это современная версия setInterval () - она выполняет указанный блок кода до того, как браузер перерисовывает изображение, позволяя запускать анимацию с подходящей частотой кадров независимо от среды, в которой она выполняется.

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

Note: Вы можете найти примеры использования requestAnimationFrame() в этом курсе — например в  Рисование графики, and Практика построения объектов.

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

function draw() {
   // Drawing code goes here
   requestAnimationFrame(draw);
}

draw();

Идея состоит в том, чтобы определить функцию, в которой ваша анимация обновляется (например, ваши спрайты перемещаются, счет обновляется, данные обновляются или что-то еще). Затем вы вызываете его, чтобы начать процесс. В конце функционального блока вы вызываете requestAnimationFrame () со ссылкой на функцию, переданной в качестве параметра, и это дает браузеру указание вызвать функцию снова при следующей перерисовке дисплея. Затем он выполняется непрерывно, поскольку код рекурсивно вызывает requestAnimationFrame ().

Note: Если вы хотите выполнить простое постоянное анимирование DOM , CSS Анимация вероятно будет быстрее. Она высисляется непосредственно внутренним кодом браузера, а не JavaScript.

Однако, если вы делаете что-то более сложное, включающее объекты, которые не доступны напрямую в the DOM (такие как 2D Canvas API или WebGL ), requestAnimationFrame() предпочтительный вариант в большинстве случаев.

Как быстро работает ваша анимация?

Плавность анимации напрямую зависит от частоты кадров анимации и измеряется в кадрах в секунду (fps). Чем выше это число, тем плавнее будет выглядеть ваша анимация до точки.

Поскольку большинство экранов имеют частоту обновления 60 Гц, максимальная частота кадров, к которой вы можете стремиться, составляет 60 кадров в секунду (FPS) при работе с веб-браузерами. Однако большее количество кадров означает больше обработки, которая часто может вызывать заикание и пропуски, также известные как пропадание кадров или заедание.

Если у вас есть монитор с частотой обновления 60 Гц и вы хотите достичь 60 кадров в секунду, у вас есть около 16,7 миллисекунд (1000/60) для выполнения кода анимации для рендеринга каждого кадра. Это напоминание о том, что вам нужно помнить об объеме кода, который вы пытаетесь запустить во время каждого прохождения цикла анимации.

requestAnimationFrame () всегда пытается приблизиться к этому волшебному значению 60 FPS, насколько это возможно. Иногда это невозможно - если у вас действительно сложная анимация и вы запускаете ее на медленном компьютере, частота кадров будет меньше. Во всех случаях requestAnimationFrame () всегда будет делать все возможное с тем, что у него есть.

Чем отличается requestAnimationFrame() от setInterval() and setTimeout()?

Давайте поговорим еще немного о том, чем метод requestAnimationFrame () отличается от других методов, используемых ранее. Глядя на наш код сверху:

function draw() {
   // Drawing code goes here
   requestAnimationFrame(draw);
}

draw();

Такой же код с использованием setInterval():

function draw() {
   // Drawing code goes here
}

setInterval(draw, 17);

Как мы уже говорили ранее, вы не указываете временной интервал для requestAnimationFrame (). Просто он работает максимально быстро и плавно в текущих условиях. Браузер также не тратит время на запуск, если по какой-то причине анимация выходит за пределы экрана и т. д.

setInterval (), с другой стороны, требует указания интервала. Мы пришли к нашему окончательному значению 17 по формуле 1000 миллисекунд / 60 Гц, а затем округлили его в большую сторону. Округление - хорошая идея; если вы округлите в меньшую сторону, браузер может попытаться запустить анимацию со скоростью, превышающей 60 кадров в секунду, и в любом случае это не повлияет на плавность анимации. Как мы уже говорили, стандартная частота обновления - 60 Гц.

В том числе временная метка

Фактическому обратному вызову, переданному в функцию requestAnimationFrame (), также может быть задан параметр: значение отметки времени, которое представляет время с момента начала работы requestAnimationFrame ().

Это полезно, поскольку позволяет запускать вещи в определенное время и в постоянном темпе, независимо от того, насколько быстрым или медленным может быть ваше устройство. Общий шаблон, который вы бы использовали, выглядит примерно так:

let startTime = null;

function draw(timestamp) {
    if (!startTime) {
      startTime = timestamp;
    }

   currentTime = timestamp - startTime;

   // Do something based on current time

   requestAnimationFrame(draw);
}

draw();

Поддержка браузерами

requestAnimationFrame () поддерживается в более поздних версиях браузеров, чем setInterval () / setTimeout (). Интересно, что он доступен в Internet Explorer 10 и выше.

Итак, если вам не тербуется поддержка старых версий IE, нет особых причин не использовать requestAnimationFrame().

Простой пример

Хватит теории! Давайте выполним упражнение с использованием requestAnimationFrame() . Создадим простую анимацию "spinner animation"—вы могли ее видеть в приложениях когда происходят задержки при ответе с сервера и т.п..

Note: Для такой простой анимации, вам следовало бы использовать CSS . Однако такой вид анимации очень полезен для демонстрации requestAnimationFrame() , вы скорее всего будете использовать этот метод когда делаете что-то более сложное, например обновление отображения игры в каждом кадре.

  1. Возьмите базовый HTML шаблон (такой как этот).

  2. Поместите пустой  <div> елемент внутри элемента <body>, затем добавьте внутрь символ ↻ . Этот символ будет действовать как spinner в нашем примере.

  3. Применитеpply следующий CSS к HTML шаблону (любым предпочитаемым способом). Он установ красный фон на странице, высоту <body> равную 100% высоты <html> , и центрирует <div> внутри <body>, по горизонтали и вертикали.

    html {
      background-color: white;
      height: 100%;
    }
    
    body {
      height: inherit;
      background-color: red;
      margin: 0;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    div {
      display: inline-block;
      font-size: 10rem;
    }
  4. Разместите  <script> элемент перед </body> .

  5. Разместите следующий JavaScript код в  <script> . Здесь вы сохраняете ссылку на <div> внутри, устанавливаете дяпеременной rotateCount значение 0, устанавливаете неинициализированную переменную, которая позже будет использоваться для хранения ссылки на вызов requestAnimationFrame(), и устанавливатете для переменной startTime значение null, которая будет позже использоваться для хранения времени начала requestAnimationFrame().

    const spinner = document.querySelector('div');
    let rotateCount = 0;
    let startTime = null;
    let rAF;
    
  6. Под предыдущим котом вставьте функцию draw() соторая будет использоваться для хранения нашешо кода анимации, который включает параметр timestamp :

    function draw(timestamp) {
    
    }
  7. Внутри draw () добавьте следующие строки. Они определят время начала, если оно еще не определено (это произойдет только на первой итерации цикла), и установят для параметра rotateCount значение для поворота счетчика (текущая временная метка, возьмите начальную временную метку, разделенную на три, чтобы замедлиться):

      if (!startTime) {
       startTime = timestamp;
      }
    
      rotateCount = (timestamp - startTime) / 3;
    
  8. Под предыдущей строкой внутри draw () добавьте следующий блок - он проверяет, превышает ли значение rotateCount 359 (например, 360, полный круг). Если это так, он устанавливает значение по модулю 360 (то есть остаток, оставшийся после деления значения на 360), поэтому круговая анимация может продолжаться непрерывно с разумным низким значением. Обратите внимание, что это не является строго необходимым, но легче работать со значениями от 0 до 359 градусов, чем со значениями типа «128000 градусов».

    if (rotateCount > 359) {
      rotateCount %= 360;
    }
  9. Затем, под предыдущим блоком, добавьте следующую строку, чтобы вращать spinner:
    spinner.style.transform = `rotate(${rotateCount}deg)`;
  10. В самом низу внутри функции draw () вставьте следующую строку. Это ключ ко всей операции - вы устанавливаете для переменной, определенной ранее, активный вызов requestAnimation (), который принимает функцию draw () в качестве своего параметра. Это запускает анимацию, постоянно выполняя функцию draw () со скоростью, близкой к 60 FPS.

    rAF = requestAnimationFrame(draw);
  11. Ниже, вызовите функцию draw() для запуска анимации.

    draw();

Note: Вы можете посмотреть рабочий образец на GitHub. ( исходный код.)

Очмстка вызова  requestAnimationFrame() 

Clearing a requestAnimationFrame() call can be done by calling the corresponding cancelAnimationFrame() method. (Note that the function name starts with "cancel", not "clear" as with the "set..." methods.) 

Just pass it the value returned by the requestAnimationFrame() call to cancel, which you stored in the variable rAF:

cancelAnimationFrame(rAF);

Active learning: Starting and stopping our spinner

In this exercise, we'd like you to test out the cancelAnimationFrame() method by taking our previous example and updating it, adding an event listener to start and stop the spinner when the mouse is clicked anywhere on the page.

Some hints:

  • A click event handler can be added to most elements, including the document <body>. It makes more sense to put it on the <body> element if you want to maximize the clickable area — the event bubbles up to its child elements.
  • You'll want to add a tracking variable to check whether the spinner is spinning or not, clearing the animation frame if it is, and calling it again if it isn't.

Note: Try this yourself first; if you get really stuck, check out of our live example and source code.

Throttling a requestAnimationFrame() animation

One limitation of requestAnimationFrame() is that you can't choose your frame rate. This isn't a problem most of the time, as generally you want your animation to run as smoothly as possible. But what about when you want to create an old school, 8-bit-style animation?

This was a problem, for example, in the Monkey Island-inspired walking animation from our Drawing Graphics article:

In this example, you have to animate both the position of the character on the screen, and the sprite being shown. There are only 6 frames in the sprite's animation. If you showed a different sprite frame for every frame displayed on the screen by requestAnimationFrame(), Guybrush would move his limbs too fast and the animation would look ridiculous. This example therefore throttles the rate at which the sprite cycles its frames using the following code:

if (posX % 13 === 0) {
  if (sprite === 5) {
    sprite = 0;
  } else {
    sprite++;
  }
}

So the code only cycles the sprite once every 13 animation frames.

...Actually, it's about every 6.5 frames, as we update posX (character's position on the screen) by two each frame:

if (posX > width/2) {
  newStartPos = -( (width/2) + 102 );
  posX = Math.ceil(newStartPos / 13) * 13;
  console.log(posX);
} else {
  posX += 2;
}

This is the code that calculates how to update the position in each animation frame.

The method you use to throttle your animation will depend on your particular code. For instance, in the earlier spinner example, you could make it appear to move slower by only increasing rotateCount by one on each frame, instead of two.

Active learning: a reaction game

For the final section of this article, you'll create a 2-player reaction game. The game will have two players, one of whom controls the game using the A key, and the other with the L key.

When the Start button is pressed, a spinner like the one we saw earlier is displayed for a random amount of time between 5 and 10 seconds. After that time, a message will appear saying "PLAYERS GO!!" — once this happens, the first player to press their control button will win the game.

Let's work through this:

  1. First of all, download the starter file for the app. This contains the finished HTML structure and CSS styling, giving us a game board that shows the two players' information (as seen above), but with the spinner and results paragraph displayed on top of one another. You just have to write the JavaScript code.

  2. Inside the empty <script> element on your page, start by adding the following lines of code that define some constants and variables you'll need in the rest of the code:

    const spinner = document.querySelector('.spinner p');
    const spinnerContainer = document.querySelector('.spinner');
    let rotateCount = 0;
    let startTime = null;
    let rAF;
    const btn = document.querySelector('button');
    const result = document.querySelector('.result');

    In order, these are:

    1. A reference to the spinner, so you can animate it.
    2. A reference to the <div> element that contains the spinner, used for showing and hiding it.
    3. A rotate count. This determines how much you want to show the spinner rotated on each frame of the animation.
    4. A null start time. This will be populated with a start time when the spinner starts spinning.
    5. An uninitialized variable to later store the requestAnimationFrame() call that animates the spinner.
    6. A reference to the Start button.
    7. A reference to the results paragraph.
  3. Next, below the previous lines of code, add the following function. It simply takes two numbers and returns a random number between the two. You'll need this to generate a random timeout interval later on.

    function random(min,max) {
      var num = Math.floor(Math.random()*(max-min)) + min;
      return num;
    }
  4. Next add  the draw() function, which animates the spinner. This is very similar to the version from the simple spinner example, earlier:

    function draw(timestamp) {
      if(!startTime) {
       startTime = timestamp;
      }
    
      rotateCount = (timestamp - startTime) / 3;
     
      if(rotateCount > 359) {
        rotateCount %= 360;
      }
    
      spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
      rAF = requestAnimationFrame(draw);
    }
  5. Now it is time to set up the initial state of the app when the page first loads. Add the following two lines, which simply hide the results paragraph and spinner container using display: none;.

    result.style.display = 'none';
    spinnerContainer.style.display = 'none';
  6. Next, define a reset() function, which sets the app back to the original state required to start the game again after it has been played. Add the following at the bottom of your code:

    function reset() {
      btn.style.display = 'block';
      result.textContent = '';
      result.style.display = 'none';
    }
  7. Okay, enough preparation!  It's time to make the game playable! Add the following block to your code. The start() function calls draw() to start the spinner spinning and display it in the UI, hides the Start button so you can't mess up the game by starting it multiple times concurrently, and runs a setTimeout() call that runs a setEndgame() function after a random interval between 5 and 10 seconds has passed. The following block also adds an event listener to your button to run the start() function when it is clicked.

    btn.addEventListener('click', start);
    
    function start() {
      draw();
      spinnerContainer.style.display = 'block';
      btn.style.display = 'none';
      setTimeout(setEndgame, random(5000,10000));
    }

    Note: You'll see this example is calling setTimeout() without storing the return value. (So, not let myTimeout = setTimeout(functionName, interval).) 

    This works just fine, as long as you don't need to clear your interval/timeout at any point. If you do, you'll need to save the returned identifier!

    The net result of the previous code is that when the Start button is pressed, the spinner is shown and the players are made to wait a random amount of time before they are asked to press their button. This last part is handled by the setEndgame() function, which you'll define next.

  8. Add the following function to your code next:

    function setEndgame() {
      cancelAnimationFrame(rAF);
      spinnerContainer.style.display = 'none';
      result.style.display = 'block';
      result.textContent = 'PLAYERS GO!!';
    
      document.addEventListener('keydown', keyHandler);
    
      function keyHandler(e) {
        let isOver = false;
        console.log(e.key);
        
        if (e.key === "a") {
          result.textContent = 'Player 1 won!!';
          isOver = true;
        } else if (e.key === "l") {
          result.textContent = 'Player 2 won!!';
          isOver = true;
        }
    
        if (isOver) {
          document.removeEventListener('keydown', keyHandler);
          setTimeout(reset, 5000);
        }
      };
    }

    Stepping through this:

    1. First, cancel the spinner animation with cancelAnimationFrame() (it is always good to clean up unneeded processes), and hide the spinner container.
    2. Next, display the results paragraph and set its text content to "PLAYERS GO!!" to signal to the players that they can now press their button to win.
    3. Attach a keydown event listener to the document. When any button is pressed down, the keyHandler() function is run.
    4. Inside keyHandler(), the code includes the event object as a parameter (represented by e) — its key property contains the key that was just pressed, and you can use this to respond to specific key presses with specific actions.
    5. Set the variable isOver to false, so we can track whether the correct keys were pressed for player 1 or 2 to win. We don't want the game ending when a wrong key was pressed.
    6. Log e.key to the console, which is a useful way of finding out the key value of different keys you are pressing.
    7. When e.key is "a", display a message to say that Player 1 won, and when e.key is "l", display a message to say Player 2 won. (Note: This will only work with lowercase a and l — if an uppercase A or L is submitted (the key plus Shift), it is counted as a different key!) If one of these keys was pressed, set isOver to true.
    8. Only if isOver is true, remove the keydown event listener using removeEventListener() so that once the winning press has happened, no more keyboard input is possible to mess up the final game result. You also use setTimeout() to call reset() after 5 seconds — as explained earlier, this function resets the game back to its original state so that a new game can be started.

That's it—you're all done!

Note: If you get stuck, check out our version of the reaction game (see the source code also).

Conclusion

So that's it — all the essentials of async loops and intervals covered in one article. You'll find these methods useful in a lot of situations, but take care not to overuse them! Because they still run on the main thread, heavy and intensive callbacks (especially those that manipulate the DOM) can really slow down a page if you're not careful.

In this module