Привіт усім! В цій статті я розповім, як оперувати виконанням коду на більш нижчому рівні. Якщо ви розумієте, що у вашому коді є макро- і мікрозадачі, то ви можете більше передбачити поведінку коду і послідовність виконання певних функцій та методів, що значно полегшує роботу в оптимізації вашого додатку.
Що таке макро- і мікрозадачі
Макро: 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
Розберемо отриманий результат:
console.log()
— завжди виконуються першими, тому що це теж I/O-операція і вона — завжди перша після ініціалізації коду. Це відбувається через те, що таймери виконуються після того, як будуть назначені — їх поведінку контролює poll-фаза, а доnext tick queue
черга ще не дійшла.process.nextTick()
— виконується другим, тому що він спрацьовує вnext tick queue
.queueMicrotask()
є альтернативоюprocess.nextTick()
і виконується в тій самій черзі, де іPromise
, тому виконується завжди післяprocess.nextTick()
. Але зPromise
виконуються на рівних правах, тобто залежно лише від послідовності в коді.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
.
Отже, ми розглянули детально мікро- і макрозадачі. І тепер кожен, прочитавши цю статтю і протестуючи код в прикладах, зможе чітко відповісти на питання стосовно послідовності виконання різних задач.
За що вам дякую і бажаю всім продуктивного кодування 😉
Цей матеріал – не редакційний, це – особиста думка його автора. Редакція може не поділяти цю думку.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: