Урок 11

Асинхронность: async/await и event loop

async/await

async перед функцией делает её асинхронной. Внутри можно использовать await:

async function load() {
    const res = await fetch('/api/data');
    const data = await res.json();
    return data;
}

await приостанавливает функцию, пока промис не разрешится.

Асинхронная функция возвращает промис

async function getValue() {
    return 42;
}

getValue().then((v) => console.log(v)); // 42

Обработка ошибок

async function load() {
    try {
        const data = await fetchData();
        return data;
    } catch (error) {
        console.error('Ошибка:', error);
        throw error;
    }
}

try/catch работает с await, как с синхронным кодом.

Последовательность

async function process() {
    const user = await getUser();
    const posts = await getPosts(user.id);
    const comments = await getComments(posts[0].id);
    return comments;
}

Каждый шаг ждёт предыдущий. Это может быть медленно, если запросы независимы.

Параллельность

async function process() {
    const [user, posts, comments] = await Promise.all([
        getUser(),
        getPosts(),
        getComments(),
    ]);
    return { user, posts, comments };
}

Все три запроса идут одновременно.

Event loop

JavaScript однопоточный, но асинхронный. Event loop — механизм, который позволяет выполнять асинхронный код.

Очереди

  1. Call stack — стек вызовов
  2. Microtasks — промисы, queueMicrotask, MutationObserver
  3. Macrotasks — setTimeout, setInterval, I/O, UI-события

Порядок выполнения

  1. Синхронный код выполняется полностью
  2. Затем — все microtasks
  3. Затем — одна macrotask
  4. Снова все microtasks
  5. Следующая macrotask
  6. И так далее

Пример

console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');

// 1, 4, 3, 2
  • Синхронный код: 1, 4
  • Microtasks: 3
  • Macrotasks: 2

Микрозадачи важнее

setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));

// promise
// timeout

Промисы всегда раньше таймеров.

Асинхронный код не блокирует

console.log('начало');

setTimeout(() => console.log('середина'), 0);

console.log('конец');

// начало, конец, середина

setTimeout не блокирует выполнение.

Топ-левел await

В модулях:

const data = await fetch('/api/data').then((r) => r.json());

Работает только в ES-модулях и современных окружениях.

Полезные приёмы

Задержка

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
await sleep(1000);

Таймаут для промиса

function withTimeout(promise, ms) {
    return Promise.race([
        promise,
        new Promise((_, reject) =>
            setTimeout(() => reject(new Error('Timeout')), ms)
        ),
    ]);
}

Последовательная обработка

for (const item of items) {
    await process(item); // по одному
}

Параллельная

await Promise.all(items.map((item) => process(item)));

С ограничением:

async function processWithLimit(items, limit) {
    const results = [];
    const executing = [];

    for (const item of items) {
        const p = process(item).then((r) => {
            executing.splice(executing.indexOf(p), 1);
            return r;
        });
        results.push(p);
        executing.push(p);

        if (executing.length >= limit) {
            await Promise.race(executing);
        }
    }

    return Promise.all(results);
}

Итоги

  • async/await — синтаксис поверх промисов
  • try/catch для ошибок
  • Promise.all для параллельности
  • Event loop: stack → microtasks → macrotasks
  • Промисы раньше таймеров
  • Ограничивай параллельность при нагрузке