Отдельное место в асинхронном мире занимают таймеры. Они позволяют отложить выполнение какой-либо функции "на потом". Наиболее важная функция для работы с таймерами — setTimeout(f, delay)

const f = () => console.log('hey!');
setTimeout(f, 1000);

В коде выше функция f выполнится не раньше, чем через секунду. Об этом нам говорит второй параметр, в который передаётся время, указанное в миллисекундах, после которого запустится функция, указанная первым параметром. По историческим причинам у таймеров есть минимальная задержка, которую они соблюдают всегда, и она равна четырём миллисекундам. Другими словами, нет разницы между вызовами setTimeout(f, 1), setTimeout(f, 3) и setTimeout(f, 4) — во всех этих случаях минимальная задержка равна 4.

Для чего нужны таймеры? У них много разных применений. Если говорить про браузер, то это могут быть автоматически скрываемые элементы: например, нотификации. Другой пример — это регулярный (например, раз в 5 секунд) Ajax-запрос для получения новых данных. На сервере таймеры используются реже, но тоже встречаются: с помощью них можно разбить объёмную синхронную операцию на несколько кусков, давая возможность выполниться другому коду.

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

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

const f = () => console.log('hey!');
console.log('before timeout');
setTimeout(f, 1000);
console.log('after timeout');
// скрипт не заканчивается, а дожидается выполнения таймеров

Запуск:

$ node index.js

before timeout
after timeout
hey!

Попробуйте ответить на такой вопрос. Могут ли таймеры гарантировать точный запуск через указанный промежуток времени? На самом деле не могут. Все зависит от того, что выполняется прямо сейчас. Проверкой таймеров занимается рантайм в тот момент, когда в текущем стеке вызовов не осталось кода. Если запустить тяжелое вычисление, которое не прекращается долго, то все колбеки, все таймеры, будут ждать пока вычисление закончится. Фактически это означает, что в таймерах задается минимальное время, после которого их можно запускать.

Эта особенность имеет два важных следствия:

  • Старайтесь минимизировать время выполнения долгих вычислений. Например, их можно разбивать на шаги.
  • Не рассчитывайте на точность времени вызова. Оно всегда будет отличаться в большую сторону.

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

const f = () => console.log('hey!');
console.log('before timeout');
// В браузере идентификатор таймера это числовое значение
// В node.js это объект
const timerId = setTimeout(f, 1000);
console.log('after timeout');
clearTimeout(timerId);

Запуск:

$ node index.js

before timeout
after timeout

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

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

const f = message => console.log(message);
console.log('before timeout');
setTimeout(f('hey!'), 1000);
console.log('after timeout');

Запуск:

$ node index.js

before timeout
hey!
timers.js:390
    throw new ERR_INVALID_CALLBACK();
    ^

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function

До последнего лога дело не дошло, потому что скрипт упал на вызове setTimeout, так как он ожидал на вход функцию, а пришла не функция (вызов в примере вернул значение undefined).

Запомнить данные внутри функции можно тремя способами:

Дополнительные параметры в setTimeout

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

const f = (a, b) => console.log(a + b);
setTimeout(f, 1000, 5, 8);
// =>  13

Функция-обёртка

Наиболее распространённый способ — создание функции-обёртки. Такой способ лучше предыдущего из-за его прозрачности: сразу видно, что происходит.

const f = (a, b) => console.log(a + b);
setTimeout(() => f(5, 8), 1000);
// =>  13

bind

Последний способ — использовать функцию bind. Основное предназначение этой функции — смена контекста функции. Но как побочный эффект она может использоваться для частичного применения:

const f = (a, b) => console.log(a + b);
// Первый параметр null потому что контекст не меняется
setTimeout(f.bind(null, 5, 8), 1000);
// =>  13

Вызов этой функции возвращает новую функцию с применёнными аргументами.

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

setInterval

Функция setInterval имеет точно такую же сигнатуру, как и setTimeout. Смысл аргументов — тот же самый. Разница в том, что setInterval автоматически запускает функцию не один раз, а до тех пор, пока её явно не остановят через clearInterval. Время между запусками равно переданному второму параметру.

const id = setInterval(() => console.log(new Date()), 5000);
setTimeout(() => clearInterval(id), 16000);

// $ node index.js
// 2019-06-05T19:05:28.149Z
// 2019-06-05T19:05:33.172Z
// 2019-06-05T19:05:38.177Z

Таймер можно остановить изнутри, передав в колбек его id.

let counter = 0;
const id = setInterval(() => {
  counter += 1;
  if (counter === 4) {
    clearInterval(id);
    return;
  }
  console.log(new Date());
}, 5000);

Дополнительные материалы

  1. setTimeout
  2. setInterval
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →