Рубріки: Back-end

Опануй справжню силу в Node.js: розбираємо мікро- та макрозадачі на прикладах

Владислав Хирса

Привіт усім! В цій статті я розповім, як оперувати виконанням коду на більш нижчому рівні. Якщо ви розумієте, що у вашому коді є макро- і мікрозадачі, то ви можете більше передбачити поведінку коду і послідовність виконання певних функцій та методів, що значно полегшує роботу в оптимізації вашого додатку.

Що таке макро- і мікрозадачі

Макро: setTimeout, setImmediate, setInterval, I/O, UI rendering.

Мікро: Promise, process.nextTick, queueMicrotask, і на фронті ми маємо наглядача за DOM елементами MutationObserver.

Різниця між ними в тому, що мікрозадачі мають пріоритет перед макрозадачами.

Вони виконуються в першу чергу, лише після того, як виконуються всі мікрозадачі. Event loop переходить до черги макрозадач — і потім знову після макрозадачі виконуються всі мікро-. І так по колу, поки Event queue не стане порожнім.

Як обробляються задачі?

У нас є черга подій Event queue, в якій знаходяться всі наші події з їхніми обробниками в черзі. Event loop обробляє всі події по принципу FIFO (first in first out) — тобто остання подія буде оброблена в останню чергу — все як за етикетом 🙂

Проте у нас є можливість піти поза чергою, використати, так би мовити, VIP-перепустку і обійти зареєстровані події 😉 Ми можемо це зробити завдяки декільком методам:

1. process.nextTick()

2. queueMicrotask()

3. setImmediate()

Зараз пропоную вам детально розглянути код, який наочно покаже, яка функція виконується і в якій послідовності, не дивлячись на те, де вона розташована і скільки часу потрібно для її виконання:

const fibonacci = n => {
  if(n <= 1){
    return n;
  } else {
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
}
const loging = (...args) => {
  const [ colorKey, text, fibNumber ] = args;
  const colors = {
    y:'x1b[33m%sx1b[0m',
    b:'x1b[34m%sx1b[0m',
    w:'x1b[37m%sx1b[0m',
  };
  const color = colors[colorKey] || colors['w'];
  console.log(color, text + ' ' + fibonacci(fibNumber));
}
const task = async(a) => {
  const task2 = (t) =>Promise.resolve(t());
  return await task2(fibonacci.bind(null, a));
}
const taskContainer = () => {
  console.log('x1b[32m%sx1b[0m', '--- START taskContainer ---');
  setImmediate(() =>loging('y', '2 -- setImmediate', 15)); // not regular execution
  setTimeout(() =>loging('y', '2 -- setTimeout', 15)); // not regular execution
  queueMicrotask(() =>loging('y', '2 -- queueMicrotask', 20));
  process.nextTick(() =>loging('y', '2 -- nextTick', 35));
  Promise.resolve().then(_=>loging('y', '2 -- Promise', 12));
 
  console.log('x1b[31m%sx1b[0m', '--- END taskContainer ---');
};
setImmediate(() => loging('b', '1 -- setImmediate', 15)); // not regular execution
setTimeout(() => loging('b', '1 -- setTimeout', 15)); // not regular execution
task(20).then(res => console.log('Nested task result', res));
Promise.resolve().then(_ => loging('b', '1 -- Promise', 32));
queueMicrotask(() => loging('b', '1 -- queueMicrotask', 20));
process.nextTick(() => loging('b', '1 -- nextTick', 35));
console.log('x1b[34m%sx1b[0m', `1 -- log ${fibonacci(10)}`);
taskContainer();

Результат:

Console output:

1 -- log 55

--- START taskContainer ---

--- END taskContainer ---

1 -- nextTick 9227465

2 -- nextTick 9227465

1 -- Promise 2178309

1 -- queueMicrotask 6765

2 -- queueMicrotask 6765

2 -- Promise 144

Nested task result 6765

1 -- setTimeout 610

2 -- setTimeout 610

1 -- setImmediate 610

2 -- setImmediate 610

Розберемо отриманий результат:

  1. console.log() — завжди виконуються першими, тому що це теж I/O-операція і вона — завжди перша після ініціалізації коду. Це відбувається через те, що таймери виконуються після того, як будуть назначені — їх поведінку контролює poll-фаза, а до next tick queue черга ще не дійшла.
  2. process.nextTick() — виконується другим, тому що він спрацьовує в next tick queue.
  3. queueMicrotask() є альтернативою process.nextTick() і виконується в тій самій черзі, де і Promise, тому виконується завжди після process.nextTick(). Але з Promise виконуються на рівних правах, тобто залежно лише від послідовності в коді.
  4. setTimeout/setImmediate() — це таймери, вони є макрозадачами, завжди виконуються після мінімальної затримки, якщо вона не вказана або, як ми знаємо, після всіх мікрозадач.

Як ми можемо використати отримані знання

Мікрозадачі ми використовуємо для того, щоб виконати асинхронну роботу коду. Це є надважливим, коли ми, наприклад, хочемо виконати функцію після ініціалізації всього коду даного файла, але до того, як весь код починає виконуватись.

Виключенням можуть бути деякі I/O-операції, вони виконуються синхроно одразу після ініціалізації, але перед next tick queue. У всіх інших випадках краще використовувати макрозадачі, тому що їх поведінка більш передбачувана.

const importantObject = {
  _name:'Vladyslav'
}
process.nextTick(() => {
  console.log('My name is ', importantObject.getName());
});
importantObject.getName = function () {
  return this._name
}

Результат:

Console output: `My name is Vladyslav`

На прикладі цього коду ми можемо побачити, як ми відклали виконання методу importantObject.getName() і виведення його результату в консоль завдяки process.nextTick(). Таким чином ми зачекали його ініціалізацію.

const importantObject = {
  _name:'Vladyslav'
}
console.log('My name is ', importantObject.getName());
importantObject.getName = function () {
  return this._name
}

Результат:

Console output: `TypeError: importantObject.getName is not a function`

Без process.nextTick() ми отримаємо помилку, бо викличемо той метод якого ще не матимемо в importantObject.


Отже, ми розглянули детально мікро- і макрозадачі. І тепер кожен, прочитавши цю статтю і протестуючи код в прикладах, зможе чітко відповісти на питання стосовно послідовності виконання різних задач.

За що вам дякую і бажаю всім продуктивного кодування 😉

Якщо ви знайшли помилку, будь ласка, виділіть фрагмент тексту та натисніть Ctrl+Enter.

Останні статті

Закон Гудгарта або як метрики змінюють цінності

«Коли вимірюваний показник стає метою, він перестає бути хорошою мірою» Закон який значною мірою відповідальний…

03.06.2025

Як приймати обдумані рішення за допомогою ChatGPT? Приклади промптів

Інколи здається, що ви врахували все. Упевненість у рішенні настільки висока, що ви вже подумки…

02.06.2025

Чи можете ви програмувати, не дивлячись на екран?

Блогер та розробник Джозеф Круз розповів, як він працює програмістом, маючи доволі серйозні проблеми із…

23.05.2025

Як швидко полегшити головний біль. Три науково доведені способи

Голова може боліти з безлічі причин. Але один з найпоширеніших різновидів — так званий головний…

22.05.2025

Цінність або дизайн?

Коли розробляється MVP, ти маєш дуже обмежені ресурси — зазвичай і по складу команди, і…

21.05.2025

Що таке Elevator Pitch і як його використати на співбесідах?

Elevator Pitch — це коротка (30–60 секунд) презентація себе, своєї ідеї або продукту. У бізнесі…

20.05.2025