Привет всем! В этой статье я расскажу, как оперировать выполнением кода на более низком уровне. Если вы понимаете, что в вашем коде есть макро- и микрозадачи, то вы можете больше предсказать поведение кода, последовательность выполнения определенных функций и методов, что значительно упрощает работу в оптимизации вашего приложения.
Что такое макро- и микрозадачи
Макро: 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
.
Итак, мы подробно рассмотрели микро- и макрозадачи. И теперь каждый, прочитав статью и протестируя код в примерах, сможет четко ответить на вопросы относительно последовательности выполнения различных задач.
За что вам спасибо и желаю всем продуктивного кодинга 😉
Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: