Рубріки: 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.


Итак, мы подробно рассмотрели микро- и макрозадачи. И теперь каждый, прочитав статью и протестируя код в примерах, сможет четко ответить на вопросы относительно последовательности выполнения различных задач.

За что вам спасибо и желаю всем продуктивного кодинга 😉

If you have found a spelling error, please, notify us by selecting that text and pressing Ctrl+Enter.

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

Токсичные коллеги. Как не стать одним из них и прекратить ныть

В благословенные офисные времена, когда не было большой войны и коронавируса, люди гораздо больше общались…

07.12.2023

Делать что-то впервые всегда очень трудно. Две истории о начале карьеры PM

Вот две истории из собственного опыта, с тех пор, когда только начинал делать свою карьеру…

04.12.2023

«Тыжпрограммист». Как люди не из ІТ-отрасли обесценивают профессию

«Ты же программист». За свою жизнь я много раз слышал эту фразу. От всех. Кто…

15.11.2023

Почему чат GitHub Copilot лучше для разработчиков, чем ChatGPT

Отличные новости! Если вы пропустили, GitHub Copilot — это уже не отдельный продукт, а набор…

13.11.2023

Как мы используем ИИ и Low-Code технологии для разработки IT-продукта

Несколько месяцев назад мы с командой Promodo (агентство инвестировало в продукт более $100 000) запустили…

07.11.2023

Университет или курсы. Что лучше для получения IT-образования

Пару дней назад прочитал сообщение о том, что хорошие курсы могут стать альтернативой классическому образованию.…

19.10.2023