Розробляючи застосунки на JavaScript, ви, можливо, вже зустрічалися з асинхронними функціями, як, наприклад, `fetch` у браузерах або `readFile` у середовищі Node.js.
Цілком ймовірно, що результати роботи цих функцій вас дивували, якщо ви використовували їх так само, як звичайні функції. Причина криється в тому, що вони є асинхронними. У цій статті ми розглянемо, що це означає і як ефективно використовувати асинхронні функції.
Огляд синхронних функцій
JavaScript – це однопотокова мова, здатна виконувати лише одну операцію в конкретний момент часу. Це означає, що якщо процесор натрапляє на функцію, виконання якої займає багато часу, JavaScript буде чекати завершення її роботи, перш ніж перейти до інших частин програми.
Більшість функцій повністю виконуються процесором. Це означає, що під час виконання таких функцій, незалежно від тривалості процесу, процесор буде повністю зайнятий. Такі функції називають синхронними. Ось приклад синхронної функції:
function add(a, b) { for (let i = 0; i < 1000000; i ++) { // Нічого не робимо } return a + b; } // Виклик функції займе деякий час sum = add(10, 5); // Процесор не зможе перейти до цього рядка, поки функція не закінчиться console.log(sum);
Ця функція має цикл, який виконується протягом тривалого часу, перш ніж повернути суму двох своїх аргументів.
Після визначення функції ми викликаємо її та зберігаємо результат у змінній `sum`. Далі виводимо значення змінної `sum` у консоль. Хоча виконання функції додавання може зайняти певний час, процесор не перейде до виведення суми, поки виконання функції не завершиться.
Переважна більшість функцій, з якими ви стикатиметесь, будуть вести себе передбачувано, як описано вище. Однак, деякі функції є асинхронними і їх поведінка відрізняється від звичайних функцій.
Знайомство з асинхронними функціями
Асинхронні функції виконують більшу частину своєї роботи поза процесором. Це означає, що хоча виконання такої функції може зайняти деякий час, процесор залишається вільним і може виконувати інші завдання.
Ось приклад асинхронної функції:
fetch('https://jsonplaceholder.typicode.com/users/1');
Для підвищення ефективності JavaScript дозволяє процесору переходити до інших завдань, які потребують його ресурсів, ще до завершення асинхронної функції.
Оскільки процесор відмовляється чекати завершення асинхронної функції, її результат не буде одразу доступний. Він перебуватиме в стані очікування. Якщо процесор спробує виконати частини програми, залежні від цього результату, ми отримаємо помилку.
Тому процесор повинен виконувати лише ті частини програми, які не залежать від результату, що очікується. Для цього в сучасному JavaScript використовуються проміси (Promise).
Що таке Promise в JavaScript?
У JavaScript Promise – це об’єкт, що представляє проміжний результат виконання асинхронної функції. Promise є основою сучасного асинхронного програмування на JavaScript.
Після створення Promise може статися одна з двох речей: він переходить у стан resolved (виконано), коли асинхронна функція успішно завершує свою роботу і повертає значення, або в стан rejected (відхилено), якщо під час виконання сталася помилка. Це етапи життєвого циклу Promise. Таким чином, ми можемо прикріпити обробники подій до Promise, які будуть викликані, коли він буде виконано (resolved) або відхилено (rejected).
Весь код, який залежить від результату асинхронної функції, можна розмістити в обробнику події, який спрацює після переходу Promise в стан resolved. Код, який обробляє помилки відхиленого Promise, також додається до відповідного обробника подій.
Ось приклад того, як ми зчитуємо дані з файлу в Node.js:
const fs = require('fs/promises'); fileReadPromise = fs.readFile('./hello.txt', 'utf-8'); fileReadPromise.then((data) => console.log(data)); fileReadPromise.catch((error) => console.log(error));
У першому рядку ми імпортуємо модуль `fs/promises`.
У другому рядку викликаємо функцію `readFile`, передаючи їй шлях до файлу та його кодування. Ця функція є асинхронною, тому вона повертає Promise. Ми зберігаємо цей Promise у змінній `fileReadPromise`.
У третьому рядку ми додали обробник події, який спрацює, коли Promise буде виконано. Це робиться шляхом виклику методу `then` для об’єкта Promise. Як аргумент для методу `then` ми передаємо функцію, яка буде виконана, коли Promise перейде в стан resolved.
У четвертому рядку ми додали обробник події, який спрацює, коли Promise буде відхилено. Це робиться шляхом виклику методу `catch` та передачі обробника помилок як аргумента.
Альтернативний підхід — використання ключових слів `async` і `await`. Розглянемо цей підхід далі.
Пояснення `async` та `await`
Ключові слова `async` і `await` можуть бути використані для написання асинхронного коду JavaScript у більш зрозумілий спосіб. У цьому розділі я поясню, як використовувати ці ключові слова та який вплив вони мають на ваш код.
Ключове слово `await` використовується для призупинення виконання функції, поки асинхронна функція не завершить свою роботу. Ось приклад:
const fs = require('fs/promises'); async function readData() { const data = await fs.readFile('./hello.txt', 'utf-8'); // Цей рядок не виконається, поки дані не будуть доступні console.log(data); } readData()
Ми використали ключове слово `await` під час виклику `readFile`. Це вказує процесору чекати, поки файл не буде прочитано, перш ніж виконати наступний рядок (`console.log`). Це гарантує, що код, який залежить від результату асинхронної функції, не буде виконано, поки результат не стане доступним.
Якщо ви спробуєте запустити наведений вище код, то зіткнетеся з помилкою. Це тому, що `await` можна використовувати лише всередині асинхронної функції. Щоб оголосити функцію асинхронною, потрібно використовувати ключове слово `async` перед оголошенням функції, ось так:
const fs = require('fs/promises'); async function readData() { const data = await fs.readFile('./hello.txt', 'utf-8'); // Цей рядок не виконається, поки дані не будуть доступні console.log(data); } // Викликаємо функцію для її виконання readData() // Цей код буде виконуватися, поки ми чекаємо на завершення readData console.log('Очікуємо завершення отримання даних')
Запустивши цей код, ви побачите, що JavaScript виконує зовнішній `console.log`, чекаючи, поки дані, зчитані з текстового файлу, стануть доступними. Коли вони стануть доступними, виконується `console.log` всередині `readData`.
Обробка помилок при використанні `async` та `await` зазвичай виконується за допомогою блоків `try/catch`. Також важливо знати, як працювати з асинхронним кодом у циклах.
`async` і `await` є доступними в сучасному JavaScript. Раніше асинхронний код писали з використанням зворотних викликів (callback).
Огляд зворотних викликів
Зворотний виклик (callback) – це функція, яка буде викликана, коли результат асинхронної операції буде доступний. Весь код, який залежить від результату, розміщується всередині зворотного виклику. Код, розташований поза зворотним викликом, не залежить від результату і може виконуватися паралельно.
Ось приклад читання файлу в Node.js:
const fs = require("fs"); fs.readFile("./hello.txt", "utf-8", (err, data) => { // У цьому зворотному виклику ми розміщуємо весь код, який потребує даних if (err) console.log(err); else console.log(data); }); // Тут ми можемо виконувати всі дії, які не залежать від результату console.log("Привіт з програми")
У першому рядку ми імпортуємо модуль `fs`. Далі викликаємо функцію `readFile` цього модуля. Функція `readFile` зчитує текст із вказаного файлу. Першим аргументом є шлях до файлу, а другим — його кодування.
Функція `readFile` зчитує дані з файлів асинхронно. Для цього вона приймає функцію як аргумент. Цей аргумент є функцією зворотного виклику (callback), яка буде викликана після зчитування даних.
Першим аргументом, переданим під час виклику функції зворотного виклику, є помилка. Якщо помилка виникла під час виконання функції, цей аргумент матиме значення. Якщо помилки не сталося, він буде невизначеним.
Другим аргументом, переданим зворотному виклику, є дані, зчитані з файлу. Код всередині цієї функції матиме доступ до даних з файлу. Код поза цією функцією не потребує цих даних, тому він може виконуватися під час очікування зчитування файлу.
Виконання наведеного вище коду дасть такий результат:
Ключові особливості JavaScript
Існує кілька ключових особливостей, які впливають на роботу асинхронного JavaScript. Вони добре описані у відео нижче:
Нижче я коротко описав дві важливі особливості.
#1. Однопотоковість
На відміну від інших мов, які дозволяють використовувати кілька потоків, JavaScript використовує лише один. Потік — це послідовність інструкцій, які логічно залежать одна від одної. Кілька потоків дозволяють програмі виконувати інший потік, коли зустрічаються операції блокування.
Однак, кілька потоків ускладнюють розуміння програм, які їх використовують. Це збільшує ймовірність появи помилок у коді, і буде важко його налагоджувати. JavaScript був розроблений як однопотокова мова для простоти. Як однопотокова мова, вона покладається на керування подіями для ефективної обробки операцій блокування.
#2. Керування подіями
JavaScript також керується подіями. Це означає, що певні події відбуваються протягом життєвого циклу програми JavaScript. Як програміст, ви можете прикріпити функції до цих подій, і кожного разу, коли подія відбувається, прикріплена функція буде викликана та виконана.
Деякі події можуть виникати через отримання результату операції блокування. В цьому випадку асоційована функція викликається з результатом.
Що слід враховувати при написанні асинхронного JavaScript
У цьому розділі я згадаю деякі моменти, які слід враховувати при написанні асинхронного JavaScript. Це включатиме підтримку браузера, кращі практики та важливість.
Підтримка браузерами
Це таблиця, яка показує підтримку промісів у різних браузерах.
Джерело: caniuse.com
У цій таблиці показано підтримку ключових слів `async` у різних браузерах.
Джерело: caniuse.com
Кращі практики
- Завжди обирайте `async/await`, оскільки це допомагає писати чистіший і зрозуміліший код.
- Оброблюйте помилки в блоках `try/catch`.
- Використовуйте `async` тільки там, де необхідно чекати на результат виконання функції.
Важливість асинхронного коду
Асинхронний код дозволяє писати більш ефективні програми, які використовують лише один потік. Це важливо, оскільки JavaScript використовується для створення вебсайтів, які виконують багато асинхронних операцій, таких як мережеві запити та читання/запис файлів на диск. Завдяки цій ефективності, такі середовища виконання, як NodeJS, стали популярними для серверних застосунків.
Заключні слова
Це була досить довга стаття, але в ній ми змогли розглянути, чим асинхронні функції відрізняються від звичайних синхронних. Ми також розглянули різні способи використання асинхронного коду, зокрема, проміси, ключові слова `async/await` та зворотні виклики.
Крім того, ми розглянули основні особливості JavaScript. В останньому розділі ми підсумували, розглянувши підтримку браузерами та кращі практики.
Далі ознайомтеся з поширеними питаннями для інтерв’ю з Node.js.