Асинхронність в JavaScript: Посібник для тих, хто хоче розібратися

image
 JavaScript легко писати. Достатньо взяти пару бібліотек або модний фреймворк, прочитати нескладний туторіал і    через пару годин у вас простий працюючий інтерфейс.
Проблеми починаються, коли інтерфейс стає складніше. Ось тут без глибокого розуміння JavaScript обійтися. Важливо, щоб навіть великий і складний інтерфейс залишався швидким і чуйним. Чуйність, як правило, досягається за рахунок використання асинхронних функцій. Спробуємо розібратися, як влаштована асинхронність в JavaScript.
 JavaScript немає багатопоточності. Незважаючи на те, що ми вже можемо повноцінно використовувати вебворкеры, них можна міняти DOM або викликати методи об'єкта window. Одним словом, не багатопоточність, а суцільне розчарування.
Причини таких обмежень зрозумілі. Уявіть собі, що два паралельних потоки наввипередки намагаються поміняти один і  сайт в DOM непередбачуваним результатом. Уявили? Мені теж стало не  собі.
image
 DOM-деревом працюють в одному потоці, щоб гарантувати цілісність і несуперечність даних, але як програмувати інтерфейс з одним потоком? Адже сама суть інтерфейсу   асинхронності. Саме для цього придумані асинхронні функції. Вони виконуються не відразу, а після настання події. Цікаво, що ці функції   не частина JavaScript-движка. Виклик setTimeout на чистому V8 призводить до помилково, так як в V8 немає такої функції. Тоді звідки ж з'являється setTimeout або requestAnimationFrame або addEventListener.?
Асинхронність всередині
Движок JavaScript схожий на м'ясорубку, нескінченно перемалывающую операції, які послідовно беруться з стека викликів (1). Код виконується лінійно і послідовно. Видалити операцію з стека не можна, можна тільки перервати потік виконання. Потік виконання переривається, якщо викликати щось типу alert або «виняток».
image
Кожна операція містить контекст   якусь область пам'яті, якою доступні дані. Контексти розташовані в пам'яті в вигляді дерева. Кожному листу в дереві доступні області видимості, які визначені в батьківських гілках і в корені (глобальній області видимості). Функції в JavaScript — це дані, вони зберігаються в пам'яті саме як дані і тому передаються як змінні або повертаються з інших функцій.
Асинхронні операції виконуються не в движку, а в оточенні (5,6). Оточення   надбудова на движком. NodeJS і Chrome для движка V8 і Firefox для Gecko. Іноді оточення ще називають web API.
Щоб створити асинхронний виклик, в web API передається посилання на функцію, яка виконається пізніше або не виконається зовсім.
 функції є свій контекст або своя область пам'яті (3), якою вона визначена. Функція має доступ до цієї області пам'яті і до всім батькам цієї області пам'яті. Такі функції називаються замиканнями. З цієї точки зору, всі функції в JavaScript   замикання, так як всі вони мають контекст.
Web API і JavaScrtipt движок працюють незалежно. Web API вирішує, який момент функція рухається далі, в черга викликів (2).
 черзі викликів потрапляють в JavaScript-движок, де виконуються по одного. Виконання відбувається в , ж порядку, в якому функції потрапляють в чергу.
Оточення самостійно вирішує, коли додати переданий їй код в черга викликів. Функції з черзі додаються в стек виконання (виконуються) не раніше, ніж стек викликів закінчить роботу над поточною функцією.
Таким чином, стек викликів працює синхронно, а web API асинхронно.
Це дуже важливо! Розробнику не потрібно самому контролювати паралельний доступ до ресурсів, асинхронну роботу за нього виконує оточення. Оточення визначають відмінності між браузером і node.js, адже на node.js ми пишемо мережеві додатки або звертаємося безпосередньо до жорсткого диска, а з Chrome перехватываем кліки кнопкам, використовуючи один і  ж движок.
 черзі викликів не можна скасовувати окремі операції. Це робиться в оточенні (removeEventListener   як приклад).
Приклади
Можна завантажити стек викликів так, щоб він працював нескінченно і наступна функція з черзі викликів не зголосилася. Спробуйте, наприклад, запустити ось такий код.
document.addEventListener. ('click', function(){
console.log('clicked')
});

while(true){
console.log('wait');
}

Оброблювач кліка не спрацює, а нескінченний цикл завантажить процесор комп'ютера. Вкладки зависне ;)
А ось інший приклад.
Клік викличе «важку» для розрахунку функцію. Після кліка в консоль пишеться start, в наприкінці виконання функції   end. Виконання функції на моєму ноутбуці займає декілька секунд. Весь час, поки виконується функція, квадратик блимає. Це означає, що анімації в CSS виконуються асинхронно JavaScript-кодом.
 що буде, якщо замість opacity міняти розмір?
Квадратик зависне на час виконання функції. Справа в те, що CSS-властивість height звертається до DOM. Як ми пам'ятаємо, до DOM можна звертатися тільки з одного потоку, щоб не було проблем з паралельним доступом.
Робимо висновок, що для анімації краще користуватися властивостями, які не змінюють DOM (transform, opacity і т. д.). А всю важку роботу в JavaScript краще робити асинхронно. Наприклад ось так.
Код написаний для наочності і на коліні, в бою застосовувати не рекомендується. Ми ділимо великий шматок роботи на маленькі і виконуємо асинхронно. При цьому інтерфейс не блокується. Для таких розрахунків можна користуватися веб-воркерами.
Висновок
Завдяки JavaScript ми пишемо асинхронні програми, не замислюючись про багатопоточності: цілісності і несуперечності даних. За ці переваги ми платимо величезним числом зворотних викликів, блокуванням основного потоку і постійними втратами контексту.
 те, як боротися з останньою проблемою, я розповім в наступного разу.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.