JavaScript: асинхронність
20 питань · ~20 хв · Версія v3.0
Увійдіть, щоб продовжити
Проміси, async/await, цикл подій, мікро- й макрозадачі, паралельні запити й скасування через AbortController - питання всіх рівнів, від junior до senior.
- За спробу
- 20
- У пулі
- 63
- Проходжень
- 0
- Середній бал
- -
- Пройшли на 70%+
- -
Питання для підготовки
15 питаньPromise - об'єкт-обіцянка результату, який з'явиться пізніше: відповідь сервера, прочитаний файл, таймер. Замість того щоб передавати колбек, функція одразу повертає Promise, а результат на нього «підписують».
Три стани:
- pending - очікування, результату ще немає;
- fulfilled - виконано успішно, є значення;
- rejected - відхилено, є причина (зазвичай об'єкт
Error).
Перехід з pending відбувається один раз і назавжди: виконаний Promise не стане відхиленим.
fetch('/api/user')
.then((response) => response.json())
.then((user) => console.log(user.name))
.catch((error) => console.error('Не вдалося завантажити', error))
.finally(() => hideSpinner());
Що варто знати:
then()повертає новий Promise, тому виклики можна з'єднувати в ланцюжок. Значення, повернене зthen, стає результатом наступного кроку.- Один
catch()наприкінці ловить помилку з будь-якого попереднього кроку. - Promise без обробника помилки, який відхилився, дає
unhandledrejection- у Node.js це за замовчуванням завершує процес. fetchвідхиляє Promise лише при мережевій помилці. Відповідь 404 чи 500 - це виконаний Promise зresponse.ok === false.
По суті нічим: async/await - зручніший запис тих самих Promise. Асинхронний код читається зверху вниз, як синхронний.
// then()
function loadUser(id) {
return fetch(`/api/users/${id}`)
.then((r) => r.json())
.then((user) => user.name);
}
// async/await
async function loadUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user.name;
}
Правила:
async-функція завжди повертає Promise.return 5у ній - цеPromise, що виконається з 5.awaitчекає Promise і повертає його значення. Якщо Promise відхилено,awaitкидає виняток.- Помилки ловлять звичайним
try/catch.
try {
const user = await loadUser(1);
} catch (error) {
showError(error.message);
}
Пастка - зайва послідовність:
const user = await loadUser(); // чекаємо
const posts = await loadPosts(); // і лише тоді починаємо друге
Якщо запити незалежні, їх запускають разом: const [user, posts] = await Promise.all([loadUser(), loadPosts()]);.
await на верхньому рівні (поза функцією) працює в ES-модулях.
JavaScript виконує код в одному потоці. Асинхронні дії (таймери, мережа, події) виконує середовище - браузер чи Node.js, а колбеки, що мають спрацювати після них, стають у черги. Event loop бере з черг наступне завдання, коли стек викликів порожній.
Дві черги з різним пріоритетом:
- Макрозадачі (tasks):
setTimeout,setInterval, події користувача, повідомлення з мережі. - Мікрозадачі (microtasks): колбеки Promise (
then, продовження післяawait),queueMicrotask,MutationObserver.
Порядок: виконати одну макрозадачу → виконати всі мікрозадачі, що накопичилися (включно з тими, що додалися під час виконання) → (у браузері) відмалювати сторінку → наступна макрозадача.
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
// 1, 4, 3, 2
setTimeout(fn, 0) не означає «одразу»: це макрозадача, і вона чекає, поки виконаються всі мікрозадачі.
Практичні наслідки:
- Довгий синхронний код (великий цикл, важкий
JSON.parse) блокує все: кліки, анімацію, рендер. Важку роботу ділять на частини або виносять у Web Worker. - Нескінченний ланцюжок мікрозадач теж заморожує сторінку: рендер чекає, поки черга мікрозадач спорожніє.
Усі чотири приймають масив Promise і запускаються паралельно, але по-різному вирішують, коли й чим завершитися:
Promise.all- чекає всі. Результат - масив значень у тому ж порядку. Перша ж помилка відхиляє весь результат (інші Promise продовжують виконуватися, але їхні результати губляться).Promise.allSettled- чекає всі, ніколи не відхиляється. Результат - масив об'єктів{status: 'fulfilled', value}або{status: 'rejected', reason}.Promise.race- результат першого, що завершився, хай успіхом, хай помилкою.Promise.any- перший успішний. Відхиляється зAggregateError, лише якщо відхилено всі.
// Усе потрібне для сторінки - інакше показати помилку
const [user, settings] = await Promise.all([getUser(), getSettings()]);
// Розіслати сповіщення й порахувати, скільки не дійшло
const results = await Promise.allSettled(recipients.map(send));
const failed = results.filter((r) => r.status === 'rejected').length;
// Тайм-аут
const data = await Promise.race([fetchData(), sleep(5000).then(() => { throw new Error('timeout'); })]);
// Перше доступне дзеркало
const file = await Promise.any(mirrors.map((url) => fetch(url)));
Нюанс race для тайм-ауту: «програлий» запит не скасовується, він продовжує виконуватися. Для справжнього скасування потрібен AbortController (у fetch - AbortSignal.timeout(5000)).
Сам Promise скасувати не можна - лише проігнорувати його результат. Справжнє скасування роблять через AbortController: операція отримує signal і сама стежить, чи його не скасували.
const controller = new AbortController();
fetch('/api/search?q=laravel', { signal: controller.signal })
.then((r) => r.json())
.then(render)
.catch((error) => {
if (error.name === 'AbortError') return; // скасовано свідомо - не помилка
showError(error);
});
controller.abort(); // запит перервано, з'єднання закрито
Типові сценарії:
- Пошук під час набору: кожна нова літера скасовує попередній запит, щоб старіша відповідь не перезаписала новішу (класична гонка).
- Компонент зник (React-ефект, Vue
onUnmounted) - скасувати його запити й підписки. - Тайм-аут:
fetch(url, { signal: AbortSignal.timeout(5000) }). - Кілька причин скасування одразу:
AbortSignal.any([userSignal, AbortSignal.timeout(5000)]).
Сигнал працює не лише з fetch: addEventListener(type, fn, { signal }) знімає обробник при скасуванні, а у власних функціях можна перевіряти signal.aborted чи слухати подію abort:
async function processAll(items, signal) {
for (const item of items) {
signal.throwIfAborted();
await process(item);
}
}
Важливо: скасування на клієнті не зупиняє вже розпочату роботу на сервері. Якщо запит змінює дані, сервер міг їх уже змінити.
Спершу почитати
Прочитати - ще не значить знати
20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.