Метод setTimeout на языке Javascript
Функцию можно вызывать не в данный момент, а отложить, через определенный временной промежуток. Это понятие называется «планирование вызова».
Для этой цели используется два метода:
- setTimeout предоставляет возможность вызвать функцию через заданный интервал времени один раз.
- setInterval позволяет регулярно вызывать функцию, повторяя запрос через определенный промежуток времени.
Эти методы не относятся к спецификации JavaScript. Однако большинство сред выполнения кода JS имеют внутренний планировки и доступ к данным методам. Например, они поддерживаются в Node.js и во всех браузерах.
setTimeout
Синтаксис:
lettimerId=setTimeout(func|code,[delay],[arg1],[arg2],…)
Параметры:
func|code
Строка кода или функция для выполнения. Как правило, это функция. Можно передать и строку кода, но это делать не рекомендуется.
delay
Перед запуском задержка в миллисекундах (1000 мс = 1 с). По умолчанию значение – 0.
arg1, arg2…
Аргументы, которые передаются в функцию (не поддерживается в IE9-). Например, этот код вызывает sayHi() через 1 секунду:
functionsayHi(){
alert(‘Привет’);
}setTimeout(sayHi,1000);
С аргументами:
functionsayHi(phrase,who){
alert(phrase+’, ‘+who);
}setTimeout(sayHi,1000,»Привет»,»Джон»);// Привет, Джон
Если первый аргумент выступает строкой, то JavaScript создаст из неё функцию. Это тоже будет работать:
setTimeout(«alert(‘Привет’)»,1000);
Однако использование строк не рекомендуется. Для этой цели используйте функции:
setTimeout(()=>alert(‘Привет’),1000);
Передавайте функцию, но не запускайте её. Новички иногда делают ошибки – после функции добавляют скобки ( ):
// не правильно!
setTimeout(sayHi(),1000);
Это не работает, потому что setTimeout ожидает ссылку на функцию. Здесь sayHi() запускает выполнение функции, и результат выполнения отправляется в setTimeout. В этом примере результатом выполнения sayHi() является undefined (поскольку функция ничего не возвращает), поэтому ничего не планируется.
Отмена через clearTimeout
Вызов setTimeout возвращает «идентификатор таймера» timerId. Его можно использовать для отмены последующего выполнения.
Синтаксис для отмены:
lettimerId=setTimeout(…);
clearTimeout(timerId);
В коде ниже планируем вызов функции, после чего отменяем его (просто передумали). Но ничего не происходит:
lettimerId=setTimeout(()=>alert(«ничего не происходит»),1000);
alert(timerId);// идентификатор таймераclearTimeout(timerId);
alert(timerId);// тот же идентификатор (не принимает значение null после отмены)
Как мы видим из вывода alert, в веб-браузере идентификатором таймера — число. В других средах это может быть что-то ещё. К примеру, Node.js возвращает объект таймера с дополнительными методами.
Отметить еще раз! Нет единой спецификации на данные методы, поэтому такое поведение считается нормальным. Для браузеров таймеры описаны в разделе таймеров стандарта HTML5.
setInterval
Метод setInterval имеет точно такой же синтаксис как setTimeout:
lettimerId=setInterval(func|code,[delay],[arg1],[arg2],…)
Все аргументы имеют такое же значение. Однако отличие данного метода от setTimeout в том, что функция запускается не один раз, а через заданный интервал времени. Чтобы остановить дальнейшее выполнение функции, нужно вызвать clearInterval(timerId).
Приведем пример с выводом сообщения каждые 2 секунды. Через 5 секунд вывод будет прекращаться:
/ повторить с интервалом 2 секунды
lettimerId=setInterval(()=>alert(‘tick’),2000);// остановить вывод через 5 секунд
setTimeout(()=>{clearInterval(timerId);alert(‘stop’);},5000);
Во время показа alert время тоже идёт. В большинстве веб-браузеров, включаяFirefox и Chrome, внутреннийсчётчикпродолжаеттикать во время показа alert/confirm/prompt. Поэтому, если вы запустите код выше и подождёте с закрытием alert пару секунд, то следующий alert будет показан, как только будет закрыт предыдущий. Интервал времени между сообщениями alert будет короче, чем две секунды.
Рекурсивный setTimeout
Существует 2 способа запускать что-то регулярно.
- Первый — setInterval.
- Второй — рекурсивный setTimeout.
Например:
/** вместо:
lettimerId = setInterval(() =>alert(‘tick’), 2000);
*/lettimerId=setTimeout(functiontick(){
alert(‘tick’);
timerId=setTimeout(tick,2000);// (*)
},2000);
Метод setTimeout планирует следующий вызов сразу после окончания текущего (*). Рекурсивный setTimeout является более гибким методом, чем setInterval. С его помощью следующий вызов может задаваться по-разному в зависимости от результатов прошлого.
Например, нужно написать сервис, который отправляет запрос с целью получения данных на сервер каждые пять секунд, однако,если сервер перегружен, то нужно увеличить интервал запросов до 10, 20, 40 секунд… Смотрите псевдокод:
letdelay=5000;
lettimerId=setTimeout(functionrequest(){
…отправить запрос…if(ошибка запроса из-за перегрузки сервера){
// увеличить интервал для следующего запроса
delay*=2;
}timerId=setTimeout(request,delay);
},delay);
В том случае,если функции, которыемыпланируемявляютсяресурсоемкими и требуют времени, то можно измерить время, требующееся на выполнение, и спланировать дальнейший вызов раньше или позже.
Рекурсивный setTimeout позволяет более точно задать задержку между выполнениями, в отличие от setInterval.
Сравним оба фрагмента кода. Первый применяет setInterval:
let i =1;
setInterval(function(){
func(i);
},100);
Второй — рекурсивный setTimeout:
let i =1;
setTimeout(functionrun(){
func(i);
setTimeout(run,100);
},100);
Для setInterval внутренний планировщик будет выполнять func(i) каждые 100 мс:
Реальная задержка между вызовами func посредством setInterval меньше, чем она указана в коде! Это нормально, так как время, затраченное на выполнение func, будет использовать часть указанного интервала времени.
Не исключено, что выполнение func окажется дольше, чем ожидается, и займёт больше 100 мс. В таком случае движок ждет завершения выполнения func, после чего проверяет планировщик и, если время истекло, сразу же запускает его снова.
Взгляните на изображение, показывающее процесс работы рекурсивного setTimeout:
Рекурсивный setTimeout гарантирует фиксированную задержку (в примере 100 мс). Все потому, что новый вызов запланирован в конце предыдущего.
Сборка мусора и колбэк setTimeout/setInterval
Если функция передается в setInterval/setTimeout, на неё создается внутренняя ссылка и сохраняется в планировщике. Таким образомпредотвращаетсяпопаданиефункции в сборщик мусора (и неважно, веду тли на нее другие ссылки).
// функция остаётся в памяти до тех пор, пока планировщик обращается к ней
setTimeout(function(){…},100);
Для setInterval функция сохраняется в памяти до тех пор, пока вызван clearInterval. Однако нужно быть готовому и к обратному эффекту. Функция ссылается на внешнее лексическое окружение, поэтому до тех пор пока она существует, внешние переменные тоже будут существовать. Они могут занимать даже больше памяти, чем функция. Поэтому, если регулярный вызов функции больше не требуется, то лучше его отменить, даже если функция маленькая.
setTimeout с нулевой задержкой
Особый вариант использования: setTimeout(func, 0) или setTimeout(func). Это планирует вызов func максимально быстро. Однако планировщик будет вызывать функцию лишь после завершения выполнения текущего кода.
Поэтому вызов функции будет запланирован только после выполнения текущего кода. К примеру, следующий код выводит «Привет» и затем сразу «Мир»:
setTimeout(()=>alert(«Мир»));
alert(«Привет»);
Первая строка через 0 мс помещает вызов в «календарь». Однако планировщик проверит «календарь» лишь после того, как текущий код завершится. Поэтому «Привет» выводится первым, а после него»Мир».
Минимальная задержка вложенных таймеров в браузере
В браузере существуют ограничения на частоту выполнения внутренних счетчиков. Вот что сказано стандарте HTML5: «послепятивложенныхтаймеровинтервалдолженсоставлять не менее четырёх миллисекунд.».
Для наглядности приведем пример того, что это означает. Вызов setTimeout повторно вызывает себя через 0 мс. Каждый вызов запоминает реальное время от прошлого вызова в массиве times. Какой будет реальная задержка? Посмотрим:
letstart=Date.now();
lettimes=[];setTimeout(functionrun(){
times.push(Date.now()-start);// запоминаем задержку от предыдущего вызоваif(start+100<Date.now())alert(times);// показываем задержку через 100 мс
elsesetTimeout(run);// если нужно ещё запланировать
});// пример вывода:
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
Первый таймер запускается сразу (как и значится в спецификации), после чего задержка вступает в игру, и мы видим 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 мс минимальную задержку между 5 и более вложенными вызовами setTimeout, а также для setInterval, начиная с 5 вызова. Учтите, что все методы планирования не могут гарантировать точной задержки. Например, таймер может замедляться по следующим причинам:
- Перегружен процессор.
- Работа ноутбука от аккумулятора.
- Вкладка веб-браузера в фоновом режиме.
Эти факторы могут увеличивать минимальный интервал срабатывания таймера до 300 или 1000 мс в зависимости от настроек производительности ОС и используемого браузера.