UA RU
logo
Back-end      05/10/2022

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

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

Senior Backend Developer | Node.js | NestJS | Express.js | SQL | NoSQL | AWS

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

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

Макро: 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()

Онлайн-курс DevOps engineer від Mate academy.
DevOps інженери відповідають за автоматизацію процесів розробки, тестування та випуску продукту. Завдяки цьому курсу ви швидко станете високооплачуваним спеціалістом.
Отримати знижку на курс

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

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. У всіх інших випадках краще використовувати макрозадачі, тому що їх поведінка більш передбачувана.

Онлайн-курс "Business English для проджект-менеджерів" від Vocabulaba.
Підсилите англійську для управління іноземними командами та вільної комунікації на співбесідах, презентаціях і зідзвонах. .
Детальніше про курс
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.

Онлайн-курс "Blender" від Skvot.
3D-художники переходять на Blender, а в геймстудіях зростає кількість вакансій. 22 заняття, 4 роботи у портфоліо, фідбек та кар’єрні поради від досвідчених 3D-художників, щоб стартувати в геймдеві впевнено. .
Детальніше

Цей матеріал – не редакційний, це – особиста думка його автора. Редакція може не поділяти цю думку.

Топ-5 найпопулярніших блогерів вересня

Всего просмотровВсього переглядів
305
#1
Всего просмотровВсього переглядів
305
Recruiter| Talent Acquisition Specialist
Всего просмотровВсього переглядів
111
#2
Всего просмотровВсього переглядів
111
Software Developer у FullCity Consulting
Всего просмотровВсього переглядів
61
#3
Всего просмотровВсього переглядів
61
Всего просмотровВсього переглядів
41
#4
Всего просмотровВсього переглядів
41
Всего просмотровВсього переглядів
33
#5
Всего просмотровВсього переглядів
33
Рейтинг блогерів

Найбільш обговорювані статті

Топ текстів

Ваша жалоба отправлена модератору

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: