логотип PurpleSchool

setTimeout и setInterval: планирование в JavaScript

04 августа 2023 г.
592 просмотра
фото команды
Автор

Валерий

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

Синтаксис и особенности

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

setTimeout(func, delay, …args); setInterval(func, delay, …args);

Разница между ними в том, что setTimeout задает задержку до единовременного исполнения функции, а setInterval - интервал между многократными вызовами. Планируемая функция может быть описана как внутри метода, так и объявлена в другой части кода. Для сохранения совместимости в браузере, функция может быть передана в виде строки кода и создана динамически. Например эти две строки кода в браузере будут равносильны:

setTimeout(console.log("Hello!"), 1000); setTimeout('console.log("Hello!")', 1000);

Но делать так не стоит из соображений безопасности. К тому же, при исполнении такого кода в NodeJS вылетит ошибка.

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

const sayHi = () => {
  console.log("Hi!");
};
setTimeout(sayHi, 0);
setTimeout(sayHi, -1000);
setTimeout(sayHi);

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

const sayHi = () => {
  console.log("Hi!");
};
let delay = 1000;
setTimeout(sayHi, delay);
delay = 2000;

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

const greating = (name) => {console.log(`Hi, ${name}!`)};
setTimeout(greating("John"), 1000);
//NodeJS выдаст ошибку, а в браузере не будет задержки
setTimeout(greating, 1000, "John");
//все отработает как планировалось

Отмена таймеров

Вызов setTimeout или setInterval возвращает id таймера. Передав этот идентификатор в функцию clearTimeout или clearInterval мы отменим срабатывание таймера. Id всех рассматриваемых таймеров хранятся в одном месте и обе функции отмены делают идентичную работу просто удаляя из общего хранилища таймер по идентификатору. Несмотря на это, желательно использовать для отмены setTimeout именно clearTimeout и clearInterval для setInterval, что бы повысить читаемость кода.

const sayHi = () => {console.log("Hi!");};
const id = setTimeout(sayHi, 2000);
clearInterval(id);
//таймер отменен, но при чтении не понятно что за интервал
clearTimeout(id);
//делает то же самое, но читается намного понятнее

Рекурсивный setTimeout вместо setInterval

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

let delay = 2000;
let timerId = setTimeout(function request() {
  //здесь выполняется запрос на сервер
  if ("сервер перегружен") {
    // увеличиваем задержку до следующего запроса
    // здесь мы можем это сделать потому что будет создан новый таймер
    newDelay = delay *= 2;
    timerId = setTimeout(request, newDelay);
  } else {
    //если сервер не перегружен, задаем нормальную задержку
    timerId = setTimeout(request, delay);
  }
}, delay);

Реальная длительность задержек

Передавая в setTimeout и setInterval нужное время задержки вызова функции, мы ожидаем что именно через такое количество миллисекунд вызов и произойдет, но на практике время задержки может отличаться, при чем довольно сильно. Вызвано такое поведение особенностями работы Цикла Событий (Event Loop) в браузерах и Node JS. Не будем вдаваться в подробности т.к. это тема для отдельной статьи. Для понимания причин отличия переданного значения задержки от реальных результатов работы кода нам достаточно знать, что вызов функции из таймера происходит только после завершения некоторых других операций, таких, как, например синхронные операции, события WebApi, Promise и т.п. Даже если задана задержка в 0 миллисекунд.

setTimeout(() => {
  console.log("Таймаут");
}, 0);
let myPromise = new Promise((resolve, reject) => {
  console.log("Создание промиса");
  resolve();
});
myPromise.then(() => {
  console.log("Обработка промиса");
});
console.log("Синхронный код");
//Порядок вывода в консоль будет таким:
//1: Создание промиса
//2: Синхронный код
//3: Обработка промиса
//4: Таймаут

При этом длительные операции могут сильно отсрочить срабатывание таймера, например, если это сложный расчет, сильно нагружающий процессор.

setTimeout(() => console.log('Таймер на 0.1с'), 100);
let i = 0;
for(let j = 0; j < 100000000; j++) {
  // сложная операция, занимающая больше 100 мс
  i++;
}
//В консоли наш текст появится спустя более чем 100мс

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

Заключение

Планирование в JavaScript - мощный и незаменимыми инструмент разработки интерактивных веб-приложений. Он требует внимательного и осознанного использования для обеспечения лучшей производительности и эффективности программного кода и я надеюсь что данная статья была полезна для понимания работы setTimeout и setInterval. А если Вы хотите изучить основы языка (https://purpleschool.ru/course/javascript-basics) или детально погрузиться в устройство JavaScript (https://purpleschool.ru/course/javascript-advanced) я подготовил подробные курсы.

изображение курса

Основы JavaScript

Антон Ларичев
иконка часов18 часов лекций
иконка зведочки рейтинга4.8
Основы JavaScript