У попередній картці ми для простоти уявляли, що черга одна. Насправді їх дві. Питання не в тому, чим вони відрізняються. Питання в тому, чому однієї виявилось недостатньо.
TypeScript — чому Promise випереджає setTimeout(0)
console.log('1');
setTimeout(() => console.log('2: macrotask'), 0);
Promise.resolve().then(() => console.log('3: microtask'));
console.log('4');
// Виведе: 1, 4, 3, 2
Перше питання, яке виникає майже в усіх: чому Promise виконався раніше за setTimeout(0)? Адже таймер теж уже готовий.
💡 JavaScript розділяє задачі не за швидкістю, а за роллю.
Продовження вже розпочатої роботи. У більшості коду, який ми пишемо (і практично у всьому Playwright), microtask використовується саме для продовження вже розпочатої асинхронної операції:
TypeScript — продовження вже розпочатої роботи
const text = await page.textContent();
// виконається тільки після відповіді браузера
console.log(text);
console.log(text) не нова задача, а продовження тієї самої функції після завершення textContent(). Саме тому це microtask.
Macrotask — починає нову незалежну роботу. setTimeout(() => console.log('cleanup'), 0) — колбек таймера не продовжує жоден await. Це окреме прохання: “коли буде можливість — виконай ще й це”.
Саме через цю різницю ролей JavaScript завжди спершу закінчує весь поточний ланцюжок виконання (усі microtasks), і лише потім переходить до наступної незалежної задачі (macrotask).
💡 Не варто запам’ятовувати: “microtasks виконуються раніше”. Краще запам’ятати причину: JavaScript спочатку завершує вже розпочату роботу, і лише потім починає нову.
А хто всім цим керує? Відповідальність тут теж поділена: V8 відповідає за виконання JavaScript і microtasks, libuv відповідає за event loop і macrotasks.
- Microtask queue обробляє V8. Сюди потрапляє продовження після кожного
awaitі кожного.then(). Оскільки майже всі дії Playwright (click,goto,fill,textContent) завершуються через Promise, їх продовження теж опиняється саме тут. - Macrotask queue координує event loop (libuv). Тут живуть
setTimeout,setInterval.page.waitForTimeout()зрештою використовує цей самий механізм таймерів.
(Технічна виноска: є рідкісний випадок queueMicrotask(), який дозволяє явно поставити в microtask queue будь-яку функцію, навіть таку, що нічого не “продовжує” — у 99% Playwright-коду це не зустрічається, тому основне правило вище тримається.)
🗣 Як читати вголос У попередній картці ми для простоти уявляли, що черга одна, але насправді їх дві, і питання не в тому чим вони відрізняються, а в тому чому однієї виявилось недостатньо. JavaScript розділяє задачі не за швидкістю, а за роллю. Microtask використовується для продовження вже розпочатої асинхронної операції — наприклад код після await, який просто продовжує ту саму функцію після того як textContent завершився. Macrotask навпаки починає нову незалежну роботу — колбек таймера не продовжує жоден await, це окреме прохання виконати ще щось коли буде можливість. Саме через цю різницю ролей JavaScript завжди спершу закінчує весь поточний ланцюжок виконання, усі microtasks, і лише потім переходить до наступної незалежної задачі. Не варто запам’ятовувати що мікротаски виконуються раніше як окреме правило — краще запам’ятати причину: JavaScript спочатку завершує вже розпочату роботу, і лише потім починає нову. Відповідальність за це поділена: V8 відповідає за виконання JavaScript і мікротаски, libuv відповідає за event loop і макротаски.