Пошук уроків, статей та іншого контенту
Порівняєте setTimeout, setInterval і setImmediate та навчитеся скасовувати таймери й працювати з чергою подій.
Таймери в Node.js не запускають код у точно визначений момент. Вони лише повідомляють циклу подій:
«Не виконуй цей callback раніше за вказаний час».
Коли час очікування минув, callback потрапляє до відповідної черги. Він буде виконаний лише тоді, коли:
поточний синхронний код завершиться;
цикл подій дійде до потрібної фази;
черга callback-ів буде готова до обробки.
Тому setTimeout(callback, 0) не означає «виконати негайно». Це означає «виконати не раніше, ніж цикл подій зможе це зробити».
setTimeoutsetTimeout планує одноразове виконання функції.
const timeoutId = setTimeout(() => {
console.log('Повідомлення виконано');
}, 1000);
console.log('Таймер заплановано');Приблизний результат:
Таймер заплановано
Повідомлення виконаноДругий аргумент — затримка в мілісекундах:
setTimeout(callback, delay);Якщо синхронний код блокує виконання, таймер чекатиме завершення цього коду:
setTimeout(() => {
console.log('Таймер виконано');
}, 10);
const startedAt = Date.now();
while (Date.now() - startedAt < 1000) {
// Штучно блокуємо потік приблизно на одну секунду
}
console.log('Синхронний код завершено');Результат буде приблизно таким:
Синхронний код завершено
Таймер виконаноПопри затримку 10 мілісекунд, callback не міг виконатися, поки головний потік був зайнятий циклом.
setTimeoutsetTimeout повертає ідентифікатор таймера. Його можна передати до clearTimeout:
const timeoutId = setTimeout(() => {
console.log('Цей callback не буде виконано');
}, 1000);
clearTimeout(timeoutId);
console.log('Таймер скасовано');Скасувати вже виконаний callback неможливо. clearTimeout працює лише до моменту його запуску.
Ідентифікатори таймерів потрібно зберігати, якщо таймер може знадобитися скасувати, наприклад під час:
завершення HTTP-запиту;
очищення ресурсу;
зміни стану об’єкта;
зупинки сервера або сервісу.
setIntervalsetInterval повторює виконання callback через вказаний інтервал:
let count = 0;
const intervalId = setInterval(() => {
count += 1;
console.log(`Ітерація: ${count}`);
if (count === 3) {
clearInterval(intervalId);
console.log('Інтервал зупинено');
}
}, 1000);Результат:
Ітерація: 1
Ітерація: 2
Ітерація: 3
Інтервал зупиненоДля скасування інтервалу використовується clearInterval.
setInterval не гарантує, що попередній запуск callback уже завершився. Особливо важливо це для асинхронних операцій:
const intervalId = setInterval(async () => {
console.log('Початок операції');
await new Promise((resolve) => {
setTimeout(resolve, 1500);
});
console.log('Кінець операції');
}, 1000);
setTimeout(() => {
clearInterval(intervalId);
}, 5000);Інтервал становить одну секунду, а операція триває півтори секунди. Тому нова операція може початися до завершення попередньої.
Для послідовного повторення часто краще використовувати рекурсивний setTimeout:
let running = true;
async function runTask() {
if (!running) {
return;
}
console.log('Початок операції');
await new Promise((resolve) => {
setTimeout(resolve, 1500);
});
console.log('Кінець операції');
setTimeout(runTask, 1000);
}
runTask();
setTimeout(() => {
running = false;
console.log('Подальші запуски скасовано');
}, 5000);У цьому прикладі наступний запуск планується лише після завершення поточного. Це запобігає накладанню операцій одна на одну.
setImmediatesetImmediate планує callback для виконання на наступній ітерації циклу подій, у фазі check.
setImmediate(() => {
console.log('setImmediate');
});
console.log('Синхронний код');Результат:
Синхронний код
setImmediatesetImmediate часто використовують, щоб відкласти виконання до завершення поточного синхронного коду та дозволити циклу подій обробити інші операції.
Скасувати його можна за допомогою clearImmediate:
const immediateId = setImmediate(() => {
console.log('Цей callback не буде виконано');
});
clearImmediate(immediateId);setTimeout і setImmediateУ головному модулі програми порядок між setTimeout(..., 0) і setImmediate не завжди передбачуваний:
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});Можливі різні результати:
setTimeout
setImmediateабо:
setImmediate
setTimeoutЦе залежить від того, коли саме процес почав виконання і коли цикл подій дійшов до відповідних фаз.
У callback-і операції введення-виведення порядок зазвичай передбачуваніший:
const fs = require('node:fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout усередині I/O');
}, 0);
setImmediate(() => {
console.log('setImmediate усередині I/O');
});
});У цьому контексті спочатку зазвичай виконається setImmediate, а потім setTimeout, оскільки callback setImmediate обробляється у фазі check після фази poll, де завершується операція читання.
Не варто покладатися на конкретний порядок між цими функціями без урахування контексту виконання.
Node.js виконує JavaScript в одному основному потоці, але може передавати операції введення-виведення операційній системі або внутрішньому пулу потоків. Цикл подій координує повернення результатів і запуск callback-ів.
Спрощено можна уявити такі етапи:
Виконується поточний синхронний код.
Обробляються мікрозадачі.
Цикл подій переходить між фазами.
Виконуються готові callback-и таймерів, I/O та setImmediate.
Цикл повторюється, поки в процесі є активна робота.
Основні функції планують callback-и в різні частини цього процесу:
setTimeout — після мінімальної затримки, у черзі таймерів;
setInterval — повторно додає callback після завершення інтервалу;
setImmediate — у фазі check.
Таймер із нульовою затримкою все одно є асинхронним:
console.log('1');
setTimeout(() => {
console.log('3');
}, 0);
console.log('2');Результат:
1
2
3Після завершення поточного callback Node.js обробляє мікрозадачі перед переходом до наступної фази. До них належать, зокрема:
callback-и process.nextTick;
продовження Promise через .then, await та інші механізми Promise.
console.log('1');
setTimeout(() => {
console.log('5');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
process.nextTick(() => {
console.log('2');
});
console.log('4');Результат:
1
4
2
3
5Спочатку завершується синхронний код. Потім обробляється process.nextTick, після нього — Promise-мікрозадача, і лише потім стає доступним callback таймера.
process.nextTick потрібно використовувати обережно. Якщо безперервно додавати нові nextTick-callback-и, можна надовго заблокувати перехід до таймерів та I/O:
function scheduleNextTick() {
process.nextTick(scheduleNextTick);
}
// Такий код може не дозволити іншим чергам отримати керування
// scheduleNextTick();Для звичайного відкладення роботи без вимоги виконати її перед іншими чергами частіше підходять setImmediate або setTimeout.
const fs = require('node:fs');
console.log('Початок програми');
const timeoutId = setTimeout(() => {
console.log('Одноразовий таймер');
}, 1000);
let intervalCount = 0;
const intervalId = setInterval(() => {
intervalCount += 1;
console.log(`Інтервал: ${intervalCount}`);
if (intervalCount === 3) {
clearInterval(intervalId);
console.log('Інтервал скасовано');
}
}, 500);
const immediateId = setImmediate(() => {
console.log('setImmediate');
});
fs.readFile(__filename, () => {
console.log('Файл прочитано');
setImmediate(() => {
console.log('setImmediate після I/O');
});
setTimeout(() => {
console.log('setTimeout після I/O');
}, 0);
});
setTimeout(() => {
clearTimeout(timeoutId);
clearImmediate(immediateId);
console.log('Програма завершує планування таймерів');
}, 2500);У цьому прикладі:
одноразовий таймер запускається приблизно через секунду;
інтервал запускається кілька разів і потім скасовується;
setImmediate виконується після поточного синхронного коду;
читання файлу додає власний callback до циклу подій;
таймери та setImmediate після I/O потрапляють у відповідні фази;
ідентифікатори використовуються для скасування запланованих callback-ів.
setTimeout(callback, 1000);Це не гарантує запуск рівно через одну секунду. Callback може виконатися пізніше, якщо потік зайнятий або перед ним є інші callback-и.
clearIntervalАктивний інтервал продовжує виконуватися, доки його не скасувати. Це може спричинити:
зайві запити;
повторну обробку даних;
витік ресурсів;
ситуацію, коли процес не завершується.
Якщо асинхронна операція в setInterval довша за інтервал, запуски можуть перекриватися. Для послідовних операцій використовуйте ланцюжок setTimeout.
setTimeout(..., 0) виконається першимПорядок між setTimeout(..., 0) і setImmediate у головному модулі не слід вважати гарантованим.
Таймери не можуть перервати довгий синхронний цикл. Поки JavaScript виконує блокувальний код, callback-и таймерів та I/O чекатимуть.
Використовуйте:
setTimeout — для одноразового відкладеного виконання;
clearTimeout — для скасування одноразового таймера;
setInterval — для простого періодичного виконання;
clearInterval — для зупинки періодичного виконання;
рекурсивний setTimeout — для послідовних асинхронних операцій;
setImmediate — щоб виконати callback на наступному етапі циклу подій, особливо після I/O;
clearImmediate — для скасування setImmediate.
Таймер визначає мінімальну затримку, а не точний час виконання.
setTimeout виконується один раз.
setInterval повторює callback і потребує clearInterval.
setImmediate планує callback у фазі check.
Порядок setTimeout(..., 0) і setImmediate залежить від контексту.
Мікрозадачі виконуються перед переходом до наступних фаз циклу подій.
Довгий синхронний код затримує всі таймери та callback-и I/O.
Для довгих асинхронних операцій краще планувати наступний запуск після завершення попереднього.