Junior: питання на співбесіді з теми «Асинхронність»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
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-модулях.
setTimeout(fn, 0) означає не «виконай зараз», а «постав у чергу, щойно буде можливість». JavaScript у браузері виконується в одному потоці: поки працює поточний код, ніщо інше не запуститься.
console.log('1');
setTimeout(() => console.log('3'), 0);
Promise.resolve().then(() => console.log('2б'));
console.log('2');
// 1, 2, 2б, 3
Порядок такий:
- виконується весь синхронний код до кінця;
- виконуються всі мікрозадачі (колбеки Promise,
queueMicrotask); - браузер може перемалювати сторінку;
- береться наступна макрозадача - зокрема колбек таймера.
Тому колбек Promise спрацьовує раніше за таймер з нульовою затримкою.
Затримка - мінімальна, а не точна. setTimeout(fn, 100) гарантує лише, що колбек запуститься не раніше ніж за 100 мс. Якщо головний потік зайнятий важким обчисленням 2 секунди, таймер спрацює через 2 секунди.
Інші обмеження:
- вкладені таймери після п'яти рівнів отримують мінімальну затримку 4 мс;
- у фонових вкладках браузер пригальмовує таймери до разу на секунду і рідше - не можна покладатися на них для точного відліку часу;
- для анімацій є
requestAnimationFrame, синхронізований з перемальовуванням екрана.
Навіщо setTimeout(fn, 0) на практиці: відкласти роботу до моменту, коли поточна подія оброблена й DOM оновлено, або розбити довгу роботу на шматки, даючи браузеру перемальовувати сторінку між ними.
Колбек (функція зворотного виклику) - функція, яку передають іншій функції, щоб та викликала її пізніше:
button.addEventListener('click', () => console.log('клік'));
[1, 2, 3].map((n) => n * 2); // теж колбек, але синхронний
До появи Promise асинхронний код писали лише колбеками: «зроби запит і, коли буде відповідь, виклич цю функцію».
Пекло колбеків з'являється, коли кроки залежать один від одного:
getUser(id, (error, user) => {
if (error) return showError(error);
getOrders(user.id, (error, orders) => {
if (error) return showError(error);
getInvoice(orders[0].id, (error, invoice) => {
if (error) return showError(error);
render(invoice);
});
});
});
Проблеми:
- код росте вправо і погано читається;
- помилку треба перевіряти на кожному рівні окремо - легко пропустити;
- немає простого способу запустити кілька операцій паралельно й дочекатися всіх;
- функція може викликати колбек двічі чи жодного разу - і нічого вас від цього не захистить.
Те саме з Promise і async/await:
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
render(await getInvoice(orders[0].id));
} catch (error) {
showError(error);
}
Код читається зверху вниз, одна обробка помилок на всі кроки, а Promise гарантовано виконується чи відхиляється рівно один раз.
Колбеки нікуди не зникли: обробники подій, методи масивів, setTimeout - це колбеки, і для них вони доречні. Від колбеків відмовилися саме для одноразових асинхронних результатів.