Цикл подій (Event Loop) у JavaScript

javascript
Час виконання JavaScript в основному складається зі стеку викликів та купи: Куча, де об'єкти отримують пам'ять, і стек викликів, де контексти виконання вилучаються та виконуються. Контекст виконання — це група, що складається з оголошень змінних, ланцюга областей видимості та ключового слова this. Як і в будь-якій іншій мові програмування, де функції постійно вилучаються зі стеку та виконуються, в JavaScript контексти виконання вилучаються з верхівки стеку викликів та виконуються. Це змушує нас усвідомити, що виконання JS відбувається безперервно, що робить його одно поточним (single-threaded) мовою. Тож якщо час виконання JS, наприклад V8 у Chrome, насправді одно поточний, як саме виконуються зворотні виклики (callbacks)? Розглянемо наступний фрагмент коду:

console.log("Before Timeout");  
setTimeout(function cb() {  
 console.log("Out of Timer");  
}, 5000);  
console.log("After Timeout");

Очікуваний результат — це те, що вираз "After Timeout" виведеться через 5 секунд, як задано в функції setTimeout. Однак фактичний результат виглядає так:

pic

Фактичний результат вище наведеного фрагмента коду

Отже, чи не чекає час виконання на функцію timeout протягом 5 секунд перед тим, як виконати наступну інструкцію? У випадку синхронного виконання це мало б бути так. Чи означає це, що виконання асинхронне? Дивно.

Відповідь на всі наші питання проста: Час виконання JS безумовно синхронний, але браузер виконує багато інших операцій у фоновому режимі. Браузер, звісно, не є тільки середовищем виконання. Chrome не просто є двигуном V8. Він має багато інших компонентів. Якщо заглянути в двигун V8, ви також побачите, що функція setTimeout() не є його частиною. Якщо поглянути уважно, кілька інших функцій, наприклад AJAX-запити, також не є частиною традиційного часу виконання JS. То звідки ж вони беруться і як ми можемо їх використовувати в нашому коді?

Відповідь: Web API. Браузер надає Web API, які включають такі функції, як setTimeout(), fetch() тощо. Вони дозволяють нам виконувати багато асинхронних операцій.

Закулісся

Під час виконання контексту виконання у стеку викликів, коли викликається функція setTimeout(), вона активує Web API. Це запускає таймер у фоновому режимі, який викликає функцію зворотного виклику (callback function) після зазначеного часу. Як тільки таймер завершується, функція зворотного виклику додається до черги зворотних викликів (callback queue). Черга зворотних викликів перевіряє, чи порожній стек викликів. Якщо стек порожній, функція зворотного виклику потрапляє в стек, а якщо він не порожній, то функція чекає в черзі, поки стек не стане порожнім. Цю роботу виконує Event Loop (Цикл подій).

pic

Цикл подій (Event Loop)

Розглянемо наступний фрагмент коду:

setTimeout(function cb() {  
 console.log("Out of Timer");  
}, 0);  
console.log("hey1")  
console.log("hey2")  
console.log("hey3")

Оскільки функція setTimeout вказує, що зворотний виклик (callback) має бути виконаний після 0 часу (майже одразу), ми очікуємо, що виведення буде в такому порядку: "Out of Timer", "hey1", "hey2", "hey3". Однак фактичний результат явно інший.

pic

Фактичний результат вище наведеного фрагмента коду

Причина цього результату була вже згадана вище. Функція зворотного виклику (callback function) викликається лише після того, як стек викликів стає порожнім. Час виконання активує API setTimeout(), і одразу продовжує виконання наступної інструкції. Хоча таймер браузера завершується майже одразу, функція зворотного виклику все ще чекає, поки стек викликів стане порожнім. Тому результат такий.
javascript
Це призводить до того, що функція setTimeout() планує виконання функції зворотного виклику (callback function) на будь-який час після зазначеного часу таймера, але не обов'язково відразу після цього часу.

Це майже те саме, як і те, що кожна функція Web API викликає концепцію асинхронних операцій, які обробляються в JS. Функція fetch також є частиною Web API, яку надає браузер. Вона внутрішньо створює XMLHttpRequest до необхідного сервера. Однак ключова відмінність полягає в тому, що fetch надає більш гнучкий інтерфейс на основі обіцянок (promises) для обробки асинхронних HTTP-запитів.

Вищезазначене обговорення є передумовою до одного з найважливіших концептів сучасного JavaScript: Обіцянок (Promises) та асинхронних функцій (Async Functions). Більше про це в наступній статті.

Дякую за увагу.

Перекладено з: The Event Loop in JavaScript

Leave a Reply

Your email address will not be published. Required fields are marked *