Пошук уроків, статей та іншого контенту
Дослідимо Microtask Queue, чергу nextTick і відмінності між process.nextTick та Promise callbacks.
Node.js виконує JavaScript-код у поточному стеку викликів. Поки стек не спорожніє, жоден callback із черг асинхронних завдань не виконується.
Після завершення синхронного коду Node.js обробляє відкладені callbacks. Для цього важливо розрізняти:
чергу process.nextTick;
Microtask Queue рушія V8, куди потрапляють callbacks від Promise і queueMicrotask;
фази Event Loop, зокрема timers, poll, check.
process.nextTick не є звичайною фазою Event Loop. Це окрема черга Node.js, яка має вищий пріоритет за Microtask Queue.
До Microtask Queue у Node.js потрапляють:
callbacks, передані в Promise.prototype.then, catch або finally;
callbacks, створені через queueMicrotask.
Мікрозавдання виконуються після завершення поточного синхронного коду та перед переходом до наступної фази Event Loop.
console.log('sync: початок');
Promise.resolve().then(() => {
console.log('promise callback');
});
queueMicrotask(() => {
console.log('queueMicrotask callback');
});
console.log('sync: кінець');Результат:
sync: початок
sync: кінець
promise callback
queueMicrotask callbackСпочатку виконується весь синхронний код. Потім обробляються мікрозавдання в порядку їх додавання.
Promise callbacks і queueMicrotask використовують одну Microtask Queue. Тому їхній взаємний порядок залежить від порядку постановки в чергу:
Promise.resolve().then(() => {
console.log('promise 1');
});
queueMicrotask(() => {
console.log('microtask 1');
});
Promise.resolve().then(() => {
console.log('promise 2');
});Результат:
promise 1
microtask 1
promise 2process.nextTickprocess.nextTick додає callback до спеціальної черги Node.js:
process.nextTick(() => {
console.log('nextTick callback');
});
console.log('синхронний код');Результат:
синхронний код
nextTick callbackCallback із process.nextTick не виконується негайно. Спочатку Node.js завершує поточний синхронний код, а потім обробляє чергу nextTick.
nextTickУ Node.js черга process.nextTick має пріоритет над Microtask Queue:
console.log('sync: початок');
process.nextTick(() => {
console.log('nextTick 1');
});
Promise.resolve().then(() => {
console.log('promise 1');
});
queueMicrotask(() => {
console.log('microtask 1');
});
process.nextTick(() => {
console.log('nextTick 2');
});
console.log('sync: кінець');Результат:
sync: початок
sync: кінець
nextTick 1
nextTick 2
promise 1
microtask 1Порядок такий:
синхронний код;
усі callbacks із черги process.nextTick;
callbacks із Microtask Queue;
подальше виконання Event Loop.
Черга nextTick також обробляється в порядку додавання. Якщо callback додає новий nextTick, Node.js продовжує обробляти цю чергу перед переходом до Promise callbacks:
process.nextTick(() => {
console.log('nextTick 1');
process.nextTick(() => {
console.log('nextTick 2');
});
});
Promise.resolve().then(() => {
console.log('promise 1');
});Результат:
nextTick 1
nextTick 2
promise 1process.nextTick та Promise callbacks| Властивість | process.nextTick | Promise callback | |---|---|---| | Належить до | спеціальної черги Node.js | Microtask Queue V8 | | Пріоритет | вищий | нижчий за nextTick | | Середовище | Node.js | JavaScript і Node.js | | Виклик | process.nextTick(callback) | .then(callback), .catch(callback) | | Виконується після поточного стека | так | так |
Попри схожу поведінку, ці механізми не є взаємозамінними:
process.nextTick(() => {
console.log('Node.js nextTick');
});
Promise.resolve().then(() => {
console.log('Promise microtask');
});Результат у Node.js:
Node.js nextTick
Promise microtaskУ наступному прикладі видно розділення синхронного коду, черги nextTick і Microtask Queue:
console.log('1. синхронний код');
process.nextTick(() => {
console.log('2. nextTick');
process.nextTick(() => {
console.log('3. nextTick, доданий з nextTick');
});
});
Promise.resolve().then(() => {
console.log('4. Promise callback');
Promise.resolve().then(() => {
console.log('5. Promise callback, доданий з Promise');
});
});
queueMicrotask(() => {
console.log('6. queueMicrotask');
});
console.log('7. кінець синхронного коду');Результат:
1. синхронний код
7. кінець синхронного коду
2. nextTick
3. nextTick, доданий з nextTick
4. Promise callback
6. queueMicrotask
5. Promise callback, доданий з PromiseПояснення:
1 і 7 друкуються під час синхронного виконання.
Node.js повністю спорожнює чергу nextTick, тому 2 і 3 виконуються раніше за Promise callbacks.
Promise.resolve().then(...) і queueMicrotask(...) потрапляють до однієї Microtask Queue.
Внутрішній Promise callback додається після вже наявних мікрозавдань, тому 5 виконується останнім серед них.
Мікрозавдання виконуються не лише один раз наприкінці всього застосунку. Node.js регулярно перевіряє черги між окремими операціями Event Loop.
Спрощена модель має такий вигляд:
Node.js виконує поточний callback.
Після його завершення обробляє чергу process.nextTick.
Потім обробляє Microtask Queue.
Переходить до наступної операції або фази Event Loop.
Після наступного callback знову перевіряє ці черги.
Точні точки перевірки залежать від поточної операції та версії Node.js, але правило пріоритету залишається важливим: nextTick має вищий пріоритет, ніж Promise callbacks.
nextTickЯкщо постійно додавати нові callbacks у process.nextTick, Node.js може нескінченно обробляти цю чергу й не перейти до інших операцій Event Loop.
let counter = 0;
function scheduleNextTick() {
process.nextTick(() => {
counter += 1;
if (counter % 100000 === 0) {
console.log(`Виконано: ${counter}`);
}
scheduleNextTick();
});
}
scheduleNextTick();
setTimeout(() => {
console.log('Таймер виконано');
}, 0);Callback таймера може не виконатися, бо Node.js постійно спорожнює чергу nextTick.
Таке блокування називають starvation — голодуванням інших операцій. Воно може завадити:
таймерам;
обробці мережевих подій;
операціям введення-виведення;
іншим callbacks.
Тому не слід використовувати process.nextTick для великих циклів або довгих ланцюжків відкладених операцій.
process.nextTickprocess.nextTick доречний, коли Node.js API має викликати callback асинхронно, але ще до переходу до наступної операції Event Loop.
Наприклад, це дає змогу зробити поведінку API однаковою: callback не виконується синхронно навіть у випадку помилки або готового результату.
function readValue(callback) {
const value = 'готове значення';
process.nextTick(() => {
callback(null, value);
});
}
readValue((error, value) => {
if (error) {
console.error(error);
return;
}
console.log(value);
});queueMicrotaskPromise callbacks зручні, коли код уже використовує Promises або async/await.
queueMicrotask можна використовувати, коли потрібно явно запланувати мікрозавдання без створення Promise:
function notifyLater(callback) {
queueMicrotask(callback);
}
console.log('до');
notifyLater(() => {
console.log('callback');
});
console.log('після');Результат:
до
після
callbackУ прикладному коді варто обирати механізм відповідно до контексту, а не використовувати process.nextTick лише тому, що він має вищий пріоритет.
nextTick звичайною фазою Event Loopprocess.nextTick має окрему чергу. Він не є аналогом setTimeout або setImmediate.
nextTickУ Node.js спочатку обробляється черга process.nextTick, а потім Microtask Queue:
Promise.resolve().then(() => {
console.log('Promise');
});
process.nextTick(() => {
console.log('nextTick');
});Результат:
nextTick
Promiseprocess.nextTick синхроннимВін виконується після поточного синхронного коду:
process.nextTick(() => {
console.log('nextTick');
});
console.log('sync');Результат:
sync
nextTicknextTickБезконтрольне додавання callbacks до черги nextTick може заблокувати Event Loop і не дати виконатися таймерам та операціям введення-виведення.
queueMicrotaskВони використовують одну Microtask Queue. Їхній порядок визначає момент додавання до черги, а не тип callback.
Синхронний код завжди виконується раніше за відкладені callbacks.
process.nextTick використовує окрему чергу Node.js.
Promise callbacks і queueMicrotask використовують Microtask Queue V8.
Після завершення поточного коду Node.js спочатку обробляє nextTick, а потім мікрозавдання.
process.nextTick має вищий пріоритет за Promise callbacks.
Обидва механізми виконуються до переходу до наступних операцій Event Loop.
Надмірне використання process.nextTick може заблокувати Event Loop.
Для Promise-орієнтованого коду зручно використовувати Promise callbacks, а для явного мікрозавдання — queueMicrotask.