Хоча для створення повноцінного робочого коду може знадобитися глибоке знання мов програмування, таких як C++ і C, часто для роботи з JavaScript достатньо лише базового розуміння його можливостей.
Такі ідеї, як передача функцій зворотного виклику або створення асинхронного коду, зазвичай не є складними в реалізації, тому більшість розробників JavaScript не надто заглиблюються у внутрішні процеси. Вони просто не відчувають потреби розуміти тонкощі, які мова програмування від них приховала.
Але як розробнику JavaScript стає все важливішим розуміти, що насправді відбувається “під капотом” і як працюють ці складні механізми, які від нас абстраговані. Це дозволяє нам приймати більш виважені рішення, що може значно підвищити ефективність нашого коду.
Ця стаття присвячена одному з ключових, але часто недостатньо зрозумілих понять у JavaScript: циклу подій!
В JavaScript неможливо уникнути написання асинхронного коду, але що ж насправді означає асинхронний код? Відповідь криється у циклі подій.
Перш ніж ми зможемо зрозуміти, як функціонує цикл подій, нам необхідно з’ясувати, що таке JavaScript і як він працює!
Що таке JavaScript?
Перш ніж рухатися далі, давайте повернемось до самих основ. Що ж таке JavaScript? Можна визначити JavaScript як:
JavaScript – це високорівнева, інтерпретована, однопотокова, неблокуюча, асинхронна мова програмування, що працює паралельно.
Звучить як визначення з підручника, чи не так? 🤔
Давайте розберемося!
Ключові слова для цієї статті: однопотоковий, неблокуючий, паралельний і асинхронний.
Однопотоковий
Потік виконання – це мінімальна послідовність програмних інструкцій, якою може керувати планувальник. Однопотокова мова програмування здатна виконувати лише одне завдання або операцію в конкретний момент часу. Це означає, що вона виконує весь процес від початку до кінця, не перериваючи і не зупиняючи потік виконання.
На відміну від багатопотокових мов, де декілька процесів можуть виконуватися в різних потоках одночасно, не блокуючи один одного.
Але як JavaScript може бути однопотоковим і неблокуючим одночасно?
Що ж означає “блокування”?
Неблокуючий
Не існує чіткого визначення “блокування”. Це просто означає процеси, які повільно виконуються у потоці. Відповідно, неблокуючий означає процеси, які не гальмують виконання в потоці.
Зачекайте, чи не казав я, що JavaScript працює в одному потоці? І що він неблокуючий, тобто завдання виконуються швидко? Як таке можливо? А що з таймерами? Циклами?
Не хвилюйтеся! Ми розберемося трохи пізніше 😉.
Паралельний
Паралельне виконання означає, що код виконується одночасно в кількох потоках.
Ось тут стає по-справжньому заплутано. Як JavaScript може бути однопотоковим і виконувати код паралельно, тобто у кількох потоках?
Асинхронний
Асинхронне програмування означає, що код виконується у циклі подій. Коли відбувається блокуюча операція, запускається подія. Блокуючий код продовжує виконуватися, не гальмуючи основний потік виконання. Після завершення блокуючого коду, результат операцій ставиться у чергу та повертається назад до стеку.
Але в JavaScript є лише один потік? Що ж тоді виконує блокуючий код, дозволяючи іншому коду в потоці працювати?
Перш ніж продовжити, давайте підсумуємо:
- JavaScript є однопотоковим
- JavaScript є неблокуючим, тобто повільні процеси не зупиняють його виконання
- JavaScript є паралельним, тобто код виконується одночасно у кількох потоках
- JavaScript є асинхронним, тобто блокуючий код запускається в іншому місці.
Але вищезазначене не зовсім пояснює, як однопотокова мова може бути неблокуючою, паралельною та асинхронною?
Давайте заглибимося і розглянемо механізми виконання JavaScript, V8, можливо, там є якісь приховані потоки, про які ми не знаємо.
Рушій V8
Рушій V8 – це високопродуктивний двигун виконання відкритого коду, написаний Google на C++, який використовується для обробки веб-збірки JavaScript. Більшість браузерів використовують двигун V8 для запуску JavaScript, а також популярне середовище виконання Node.js.
Простою мовою, V8 – це програма на C++, яка приймає код JavaScript, компілює його та виконує.
V8 виконує дві основні функції:
- Виділення пам’яті купи
- Контекст виконання стеку викликів
На жаль, наші підозри не підтвердилися. V8 має лише один стек викликів, уявіть стек викликів як потік.
Один потік === один стек викликів === одне виконання за раз.
Зображення – Hacker Noon
Оскільки V8 має лише один стек викликів, як JavaScript працює паралельно та асинхронно, не блокуючи основний потік виконання?
Давайте спробуємо розібратися, написавши простий, але поширений асинхронний код і проаналізуємо його разом.
JavaScript виконує кожен рядок коду послідовно (однопотоково). Як і очікувалося, перший рядок виводиться на консоль, але чому останній рядок друкується раніше, ніж код з затримкою? Чому процес виконання не чекає на завершення коду затримки, перед тим як виконати останній рядок?
Схоже, якийсь інший потік допоміг нам виконати цей тайм-аут, оскільки ми майже впевнені, що потік може виконувати лише одне завдання в будь-який момент часу.
Давайте на мить заглянемо у вихідний код V8.
Чекайте, що??!!! У V8 немає функцій таймера, немає DOM? Немає подій? Немає AJAX? … Есссс!!!
Події, DOM, таймери і т.д. не є частиною основної реалізації JavaScript. JavaScript суворо дотримується специфікацій сценаріїв Ecma, а різні його версії часто називають відповідно до специфікацій сценаріїв Ecma (ES X).
Процес виконання
Події, таймери, запити Ajax надаються браузерами на стороні клієнта і часто називаються веб-API. Саме вони дозволяють однопоточному JavaScript бути неблокуючим, паралельним і асинхронним! Але як?
У процесі виконання будь-якої програми JavaScript є три основні компоненти: стек викликів, веб-API та черга завдань.
Стек викликів
Стек – це структура даних, де останній доданий елемент завжди видаляється першим. Його можна представити як стопку тарілок, з якої можна зняти лише верхню тарілку. Стек викликів – це структура даних стека, де завдання або код виконуються послідовно.
Розглянемо наступний приклад:
Джерело – https://youtu.be/8aGhZQkoFbQ
Коли викликається функція `printSquare()`, вона потрапляє у стек викликів, функція `printSquare()` викликає функцію `square()`. Функція `square()` потрапляє у стек і викликає функцію `multiply()`. Функція `multiply()` також потрапляє у стек. Оскільки функція `multiply()` повертає результат і є останньою функцією, яка була передана до стеку, вона першою завершує виконання та видаляється зі стеку, потім функція `square()`, а потім функція `printSquare()`.
Веб API
Тут виконується код, який не обробляється двигуном V8, щоб не блокувати основний потік виконання. Коли стек викликів зустрічає функцію веб-API, процес негайно передається до веб-API, де він виконується, звільняючи стек викликів для інших операцій.
Повернемося до нашого прикладу з `setTimeout`:
Коли ми запускаємо код, перший рядок `console.log` потрапляє до стеку, і результат виводиться майже одразу. Коли досягається тайм-аут, таймери обробляються браузером і не є частиною основної реалізації V8, вони передаються до веб-API, звільняючи стек для інших операцій.
Поки тайм-аут ще триває, стек переходить до наступного рядка дій і виконує останній `console.log`, що пояснює, чому ми отримуємо цей вивід перед виводом таймера. Коли таймер закінчується, щось відбувається. Таймер `console.log` знову з’являється у стеку викликів!
Як?
Цикл подій
Перш ніж ми перейдемо до обговорення циклу подій, давайте розглянемо чергу завдань.
Повернемося до нашого прикладу тайм-ауту. Щойно веб-API завершує виконання завдання, він не автоматично повертається до стеку викликів. Він переходить до черги завдань.
Черга – це структура даних, яка працює за принципом “першим прийшов, першим вийшов”, тому коли завдання надходять до черги, вони виходять у тому ж порядку. Завдання, виконані веб-API, передаються до черги завдань, а потім повертаються до стеку викликів для виведення результату.
Але зачекайте. Що ж таке цикл подій???
Джерело – https://youtu.be/8aGhZQkoFbQ
Цикл подій – це процес, який очікує, поки стек викликів буде очищено, перед тим як передати зворотні виклики з черги завдань до стеку. Коли стек очищено, цикл подій запускається і перевіряє чергу завдань на наявність доступних зворотних викликів. Якщо вони є, він передає їх до стеку викликів, очікує, поки стек викликів знову очиститься, і повторює той самий процес.
Джерело – https://www.quora.com/How-does-an-event-loop-work/answer/Timothy-Maxwell
Наведена вище схема ілюструє базовий робочий процес між циклом подій і чергою завдань.
Висновок
Хоча це досить просте введення, концепція асинхронного програмування в JavaScript дає достатнє розуміння того, що відбувається під капотом і як JavaScript може працювати паралельно та асинхронно, використовуючи лише один потік.
JavaScript завжди доступний для вивчення, і якщо вам цікаво, я б радив вам переглянути цей курс Udemy.