Get In Touch
admin@victoriaweb.me
Back

Асинхронний JavaScript

Асинхронний JavaScript — це підхід до написання коду, який дозволяє вашій програмі виконувати інші операції, не чекаючи завершення поточних. Замість того, щоб суворо дотримуватись послідовного виконання зверху вниз, JavaScript може обробляти кілька завдань одночасно. Це допомагає не витрачати час даремно, оптимізуючи роботу програми та підвищуючи її продуктивність.

Уявіть, що ви готуєте вечерю і, поки вода закипає, ви нарізаєте овочі. Ви не чекаєте, поки вода закипить, щоб почати наступне завдання. Подібно до цього, асинхронний JavaScript дозволяє виконувати інші операції, не чекаючи завершення попередніх.

Є три способи написання асинхронного коду, найстаріший з них – колбеки, далі придумали проміси, найновіший – це написання через async/await. Нові проекти можна писати використовуючи найновіший синтаксис, проте старші варіанти необхідно також знати, тому що вони використовуються якщо ми щось дописуєм в старий код і там всі асинхронні функції написані старим способом – колбеками. Нижче я поясню як використувавти кожен спосіб.

Найстаріший спосіб написання асинхронного коду – Колбеки

Колбек (callback) — це функція, яка передається іншій функції як аргумент і викликається після завершення якоїсь операції. Колбеки використовуються для обробки результатів асинхронних операцій.

Уявімо, що ми хочемо прочитати файл і вивести його вміст. Синхронний код виглядає так:

const fs = require('fs');

// Синхронне читання файлу
const data = fs.readFileSync('file.txt', 'utf8');
console.log(data);
console.log('Це повідомлення виведеться після читання файлу.');

Що таке const fs = require('fs');?

Цей рядок підключає модуль fs (File System) у твоєму коді. Модуль fs — це вбудований модуль Node.js, який надає функції для роботи з файловою системою. З його допомогою ти можеш:

  • Читати файли.
  • Записувати файли.
  • Видаляти файли.
  • Створювати папки.
  • І багато іншого.

readFileSync — це функція в Node.js, яка дозволяє синхронно читати файли. Це означає, що код зупиняється (блокуються) і чекає, поки файл повністю прочитається, і лише потім продовжує виконання наступних інструкцій.

Тут код чекає, поки файл прочитається, і лише потім виводить повідомлення.

Давай розглянемо прості приклади колбеків у JavaScript, щоб зрозуміти, як вони працюють у асинхронному коді. Колбеки — це функції, які передаються іншим функціям як аргументи і викликаються після завершення певної операції.

Приклад 1 асинхронного коду з колбеком:

Тепер зробимо те саме, але асинхронно за допомогою колбека:

const fs = require('fs');

// Асинхронне читання файлу з колбеком
fs.readFile('file.txt', 'utf8', function(err, data) {
    if (err) {
        console.error('Сталася помилка:', err);
    } else {
        console.log(data);
    }
});

console.log('Це повідомлення виведеться одразу, не чекаючи читання файлу.');

Тут:

  1. fs.readFile — асинхронна функція, яка читає файл і в цьому випадку код не чекає завершення читання файлу. Тому функція не блокує виконання іншого коду
  2. Після завершення читання файлу викликається колбек-функція function(err, data), яка обробляє результат.
  3. Код не чекає завершення читання файлу, і одразу виводить повідомлення "Це повідомлення виведеться одразу...".

Колбеки — це найстаріший спосіб роботи з асинхронним кодом. Вони прості для розуміння, але можуть призводити до складнощів у великих проектах. Сьогодні часто використовують більш сучасні підходи, такі як Проміси (Promises) та async/await, які роблять код чистішим і зручнішим.

Давай розглянемо прості приклади колбеків у JavaScript, щоб зрозуміти, як вони працюють у асинхронному коді. Колбеки — це функції, які передаються іншим функціям як аргументи і викликаються після завершення певної операції.

Приклад 1 простого колбеку з setTimeout

setTimeout — це вбудована функція JavaScript, яка виконує колбек через певний час.

// Колбек-функція
function showMessage() {
    console.log('Привіт, це повідомлення через 2 секунди!');
}

// Асинхронний код з колбеком
setTimeout(showMessage, 2000); // Викликає showMessage через 2 секунди (2000 мс)

console.log('Це повідомлення виведеться одразу.');

Пояснення коду:

  1. setTimeout приймає два аргументи:
    • Колбек-функцію (showMessage).
    • Час у мілісекундах (2000 мс = 2 секунди).
  2. Після 2 секунд викликається колбек showMessage.
  3. Код не блокується, і одразу виводиться "Це повідомлення виведеться одразу.".

Приклад 2 Колбеку з власною функцією

Створимо власну функцію, яка приймає колбек і викликає його після завершення операції.

// Функція, яка приймає колбек
function doHomework(subject, callback) {
    console.log(`Я виконую домашнє завдання з ${subject}.`);
    callback(); // Викликаємо колбек
}

// Колбек-функція
function finishedHomework() {
    console.log('Я закінчив домашнє завдання!');
}

// Викликаємо функцію з колбеком
doHomework('математики', finishedHomework);

Пояснення коду:

  1. Функція doHomework приймає два аргументи:
    • subject — назва предмета.
    • callback — функція, яка викликається після завершення “домашнього завдання”.
  2. Після виконання основної логіки (console.log) викликається колбек finishedHomework.

Результат виконання функції:

Я виконую домашнє завдання з математики.
Я закінчив домашнє завдання!

Приклад 3 Колбек з параметрами

Колбеки можуть приймати параметри. Наприклад, передамо результат операції в колбек.

// Функція, яка приймає колбек
function cookDish(dish, callback) {
    console.log(`Готую ${dish}...`);
    const result = `${dish} готовий!`;
    callback(result); // Викликаємо колбек і передаємо результат
}

// Колбек-функція
function serveDish(result) {
    console.log(`Подаю на стіл: ${result}`);
}

// Викликаємо функцію з колбеком
cookDish('піца', serveDish);

Пояснення коду:

  1. Функція cookDish готує страву і передає результат у колбек serveDish.
  2. Колбек serveDish приймає цей результат і виводить його.

Результат:

Готую піца…
Подаю на стіл: піца готовий!

Приклад 4 Асинхронний колбек з setTimeout

Поєднаємо setTimeout і колбеки, щоб змоделювати асинхронну операцію.

// Функція, яка приймає колбек
function fetchData(callback) {
    console.log('Починаю завантаження даних...');
    setTimeout(function() {
        const data = { name: 'John', age: 30 };
        callback(data); // Викликаємо колбек і передаємо дані
    }, 3000); // Затримка 3 секунди
}

// Колбек-функція
function processData(data) {
    console.log('Дані завантажено:', data);
}

// Викликаємо функцію з колбеком
fetchData(processData);

console.log('Цей код виконується одразу, не чекаючи завантаження даних.');

Пояснення коду:

  1. Функція fetchData симулює завантаження даних з затримкою 3 секунди.
  2. Після завершення затримки викликається колбек processData, який обробляє отримані дані.
  3. Код не блокується, і одразу виводиться "Цей код виконується одразу...".

Результат:

Починаю завантаження даних…
Цей код виконується одразу, не чекаючи завантаження даних.
Дані завантажено: { name: 'John', age: 30 }

Колбеки — це простий спосіб обробки асинхронних операцій. Вони дозволяють:

  1. Виконувати код після завершення певної операції.
  2. Передавати результати операцій у функції для подальшої обробки.
  3. Уникати блокування основного потоку виконання програми.

Проте, якщо колбеків стає багато, код може стати складним для розуміння (так зване “пекло колбеків”). У таких випадках краще використовувати проміси або async/await.

Пекло колбеків (Callback Hell)

Пекло колбеків (Callback Hell) — це ситуація, коли асинхронний код на основі колбеків стає дуже складним через велику кількість вкладених колбеків. Це призводить до:

  1. Поганої читабельності: Код стає схожим на “сходи” або “піраміду”, що важко розуміти.
  2. Складності підтримки: Додавання або зміна коду стає дуже складним.
  3. Проблем з обробкою помилок: Кожен колбек потребує окремої обробки помилок, що ускладнює код.

Приклад пекла колбеків:

doTask1(function(result1) {
    doTask2(result1, function(result2) {
        doTask3(result2, function(result3) {
            doTask4(result3, function(result4) {
                console.log('Остаточний результат:', result4);
            }, handleError);
        }, handleError);
    }, handleError);
}, handleError);

function handleError(error) {
    console.error('Помилка:', error);
}

В пеклі колбеків купа проблем: код стає “пірамідою” через вкладені колбеки, важко відстежувати, де і як обробляються помилки, додавання нових операцій робить код ще гіршим.

Як уникнути пекла колбеків?

  • Використовувати проміси (Promises)
  • Використовувати async/await

Другий спосіб написання написання асинхронного коду – Проміси

Давай розберемо тему промісів (Promises) — другого способу написання асинхронного коду в JavaScript. Проміси — це сучасний і зручний підхід для роботи з асинхронними операціями, який допомагає уникнути “пекла колбеків”.

Проміс (Promise) — це об’єкт, який представляє результат асинхронної операції. Він може бути в одному з трьох станів:

1 Pending (очікування): Початковий стан, коли операція ще не завершена.

2 Fulfilled (виконано): Операція успішно завершена.

3 Rejected (відхилено): Операція завершилася з помилкою.

Приклад 1: Простий проміс

Створимо простий проміс, який симулює асинхронну операцію.

const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const success = true; // Симулюємо успішне завершення
        if (success) {
            resolve('Дані успішно завантажено!');
        } else {
            reject('Помилка завантаження даних.');
        }
    }, 2000); // Затримка 2 секунди
});

// Обробка результату промісу
myPromise
    .then((result) => {
        console.log(result); // Виведе: "Дані успішно завантажено!"
    })
    .catch((error) => {
        console.error(error); // Виведе помилку, якщо success = false
    });

Пояснення коду:

  1. Проміс створюється і симулює асинхронну операцію за допомогою setTimeout.
  2. Якщо операція успішна (success = true), викликається resolve з повідомленням.
  3. Якщо операція невдала (success = false), викликається reject з повідомленням про помилку.
  4. Метод then обробляє успішний результат, а catch — помилки.

Приклад 2: Ланцюжок промісів

Проміси можуть бути об’єднані в ланцюжок, що дозволяє виконувати послідовні асинхронні операції.

function wait(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}

wait(1000) // Чекаємо 1 секунду
    .then(() => {
        console.log('1 секунда минула');
        return wait(2000); // Чекаємо ще 2 секунди
    })
    .then(() => {
        console.log('Ще 2 секунди минули');
        return wait(3000); // Чекаємо ще 3 секунди
    })
    .then(() => {
        console.log('Ще 3 секунди минули');
    })
    .catch((error) => {
        console.error('Сталася помилка:', error);
    });

Пояснення коду:

  1. Кожен then виконується після завершення попереднього промісу.
  2. Якщо на будь-якому етапі виникне помилка, вона буде перехоплена блоком catch.

Приклад 3: Проміси з реальними даними

Симулюємо завантаження даних з сервера за допомогою промісів.

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const data = { name: 'John', age: 30 };
            const error = false; // Симулюємо відсутність помилки
            if (!error) {
                resolve(data); // Повертаємо дані
            } else {
                reject('Помилка завантаження даних.');
            }
        }, 2000); // Затримка 2 секунди
    });
}

fetchData()
    .then((data) => {
        console.log('Дані отримано:', data);
    })
    .catch((error) => {
        console.error('Помилка:', error);
    });

Пояснення коду:

  1. Функція fetchData повертає проміс, який симулює завантаження даних.
  2. Якщо помилки немає, дані передаються в resolve.
  3. Якщо є помилка, вона передається в reject.

Приклад 4: Використання Promise.all

Promise.all дозволяє виконувати кілька асинхронних операцій паралельно і чекати, поки всі вони завершаться.

const promise1 = new Promise((resolve) => {
    setTimeout(() => resolve('Перший проміс'), 1000);
});

const promise2 = new Promise((resolve) => {
    setTimeout(() => resolve('Другий проміс'), 2000);
});

const promise3 = new Promise((resolve) => {
    setTimeout(() => resolve('Третій проміс'), 3000);
});

Promise.all([promise1, promise2, promise3])
    .then((results) => {
        console.log('Всі проміси завершено:', results);
    })
    .catch((error) => {
        console.error('Помилка:', error);
    });

Пояснення коду:

  1. Promise.all приймає масив промісів.
  2. Він чекає, поки всі проміси завершаться, і повертає масив результатів.
  3. Якщо хоча б один проміс завершиться з помилкою, викликається catch.

Проміси — це потужний інструмент для роботи з асинхронним кодом. Вони допомагають уникнути “пекла колбеків” і роблять код більш структурованим. У сучасному JavaScript проміси часто використовуються разом із async/await, що робить код ще зручнішим.

Найновіший спосіб написання асинхронного коду – Async/Await

async/await — це синтаксичний “цукор” над промісами, який робить асинхронний код більш схожим на синхронний. Він дозволяє використовувати ключові слова async і await для роботи з асинхронними операціями.

async: Використовується для оголошення асинхронної функції. Така функція завжди повертає проміс.

await: Використовується всередині асинхронної функції для очікування завершення промісу. Код зупиняється на цьому рядку, доки проміс не буде вирішено (або відхилено).

Приклад 1: Простий async/await

Створимо асинхронну функцію, яка симулює завантаження даних.

// Асинхронна функція
async function fetchData() {
    console.log('Починаю завантаження даних...');
    // Симулюємо асинхронну операцію за допомогою промісу
    const data = await new Promise((resolve) => {
        setTimeout(() => {
            resolve({ name: 'John', age: 30 });
        }, 2000); // Затримка 2 секунди
    });
    console.log('Дані завантажено:', data);
    return data;
}

// Виклик асинхронної функції
fetchData().then((result) => {
    console.log('Результат:', result);
});

Пояснення коду:

  1. Функція fetchData оголошена як асинхронна за допомогою async.
  2. Всередині функції використовується await, щоб дочекатися завершення промісу.
  3. Після завершення промісу результат зберігається у змінній data.
  4. Функція повертає дані, які можна обробити в then.

Приклад 2: Обробка помилок з try/catch

Для обробки помилок у async/await використовується блок try/catch.

async function fetchData() {
    try {
        console.log('Починаю завантаження даних...');
        const data = await new Promise((resolve, reject) => {
            setTimeout(() => {
                const success = true; // Симулюємо успішне завершення
                if (success) {
                    resolve({ name: 'John', age: 30 });
                } else {
                    reject('Помилка завантаження даних.');
                }
            }, 2000); // Затримка 2 секунди
        });
        console.log('Дані завантажено:', data);
        return data;
    } catch (error) {
        console.error('Помилка:', error);
    }
}

// Виклик асинхронної функції
fetchData();

Пояснення коду:

  1. Якщо проміс завершується успішно, результат зберігається у змінній data.
  2. Якщо проміс завершується з помилкою, вона перехоплюється блоком catch.

Приклад 3: Ланцюжок асинхронних операцій

async/await дозволяє легко писати послідовні асинхронні операції.

async function processData() {
    const data1 = await new Promise((resolve) => {
        setTimeout(() => resolve('Дані 1'), 1000);
    });
    console.log(data1);

    const data2 = await new Promise((resolve) => {
        setTimeout(() => resolve('Дані 2'), 2000);
    });
    console.log(data2);

    const data3 = await new Promise((resolve) => {
        setTimeout(() => resolve('Дані 3'), 3000);
    });
    console.log(data3);
}

processData();

Пояснення коду:

  1. Кожен await чекає завершення попередньої асинхронної операції.
  2. Код виглядає так, ніби він синхронний, що робить його легшим для розуміння.

Приклад 4: Паралельне виконання з Promise.all

Якщо потрібно виконати кілька асинхронних операцій паралельно, можна використовувати Promise.all разом з async/await.

async function fetchAllData() {
    const promise1 = new Promise((resolve) => {
        setTimeout(() => resolve('Дані 1'), 1000);
    });

    const promise2 = new Promise((resolve) => {
        setTimeout(() => resolve('Дані 2'), 2000);
    });

    const promise3 = new Promise((resolve) => {
        setTimeout(() => resolve('Дані 3'), 3000);
    });

    const results = await Promise.all([promise1, promise2, promise3]);
    console.log('Всі дані завантажено:', results);
}

fetchAllData();

Пояснення коду:

  1. Promise.all виконує всі проміси паралельно.
  2. await чекає, поки всі проміси завершаться, і повертає масив результатів.

async/await — це сучасний і зручний спосіб написання асинхронного коду. Він дозволяє уникнути складних ланцюжків then і робить код більш читабельним. Використовуйте async/await для роботи з асинхронними операціями, особливо якщо ви працюєте з промісами.

Fetch API

fetch() — це сучасний API в JavaScript, який дозволяє робити HTTP-запити (наприклад, GET, POST, PUT, DELETE) до сервера і отримувати відповіді. Він повертає проміс, що робить його зручним для використання з async/await.

Основні можливості fetch():

Надсилання запитів: Можна надсилати різні типи HTTP-запитів (GET, POST тощо).

Робота з відповідями: Можна отримувати дані у різних форматах (JSON, текст, бінарні дані).

Проста обробка помилок: Використовуючи try/catch або метод catch промісу.

Синтаксис fetch()

fetch(url, options)
    .then(response => {
        // Обробка відповіді
    })
    .catch(error => {
        // Обробка помилок
    });
  • url: Адреса, на яку відправляється запит.
  • options: Необов’язковий об’єкт з налаштуваннями (метод, заголовки, тіло запиту тощо).

Приклад 1: Простий GET-запит

Надішлемо GET-запит для отримання даних з API.

fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(response => response.json()) // Перетворюємо відповідь у JSON
    .then(data => {
        console.log('Отримані дані:', data);
    })
    .catch(error => {
        console.error('Помилка:', error);
    });

Пояснення коду:

  1. fetch надсилає GET-запит на вказану URL-адресу.
  2. Відповідь обробляється методом response.json(), який повертає проміс з даними у форматі JSON.
  3. Дані виводяться в консоль.
  4. Якщо виникає помилка, вона перехоплюється блоком catch.

Приклад 2: Використання async/await

async function fetchData() {
    try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
        const data = await response.json();
        console.log('Отримані дані:', data);
    } catch (error) {
        console.error('Помилка:', error);
    }
}

fetchData();

Пояснення коду:

  1. Використовується await, щоб дочекатися завершення запиту.
  2. Відповідь перетворюється у JSON за допомогою response.json().
  3. Дані виводяться в консоль.
  4. Помилки обробляються блоком catch.

Висновок

Асинхронний JavaScript – це ключова концепція, яка дозволяє ефективно виконувати довготривалі операції без блокування основного потоку коду. Вона особливо важлива для роботи з мережевими запитами, взаємодії з базами даних та іншими ресурсозатратними завданнями.

Різні підходи до асинхронного програмування мають свої переваги та недоліки:

Колбеки забезпечують базову асинхронність, але можуть призвести до складної вкладеності та труднощів у підтримці коду.

Проміси роблять код більш читабельним та дозволяють ефективно працювати з послідовними асинхронними операціями.

Async/Await значно спрощує роботу з асинхронним кодом, роблячи його більш схожим на синхронний, що покращує розуміння та зменшує кількість потенційних помилок.

Опанування асинхронного програмування допомагає писати швидші, ефективніші та зручніші у використанні веб-додатки.

Victoriia Hladka
Victoriia Hladka
https://victoriaweb.me