Галоўная / Артыкулы / Дзевяць модэляў обявленняў для надзеянага асінхроннага JavaScript у прымэтным выкарыстоўванні

Дзевяць модэляў обявленняў для надзеянага асінхроннага JavaScript у прымэтным выкарыстоўванні

Выучыце практычныя шаблоны Promise — паралельныя запыткі, таймауты, павторные запускі, ліміты канкуранцыі і анулювання — для стварэння стойкага, прыменныяга асінхроннага JavaScript.

1998 слоў

Вы постаўляецеся да Promises пастоянна, але калькі менш вядомых патэранаў можу ператворыць заплутаны асінхронны код у ўсё, што можна прыгадаць і легка аналізаваць.

Большасць людзей пазнае Promises за дапамою такога прыкладу:

fetch("/api/users")
.then((res) => res.json())
.then((users) => console.log(users));

Для простых скрыптав такі спосаб дзейсна ўсё, што трэба.

Проблемы пачынаюцца, калі пераходзите да прыкладоў застосоў высокага рангу.

Раптам вам патрабуецца, каб калькі запытанняў выконваліся адночасна, а не адны за іншым.

Вам патрабуецца спосаб абараныць выконанне роботы, якая больш не ўжо актуальная.

Вам патрабуецца можласць перапрыбутку, калі запыт не выйшоў.

Вам патрабуецца продаваць працю нават тады, калі толькі частка операцыі выйшла.

Вам патрабуецца запобегчы тому, каб адной і той жа вызов API выканаліся два разы праз адпадковасць.

А іноды вам патрабуецца обмежыць калькі асінхронных операцый, якія выканваліся адночасна, каб ваш бэкенд не быў перавантажаны ўсім адразу.

Этам момент, калі Promises перестае быць тэмай для пачаткавікаў і становіцца справжнім інструментам дизайна.

Нижчэй паказаны дзевяць шаблонаў, якія варта маты ў сваёй «наборцы» пад час напісання сучаснага JavaScript.

1. Адкрыць незалежныя запыткі паралельна

Аднам з простейшых практычных рашэнняў у асінхронным кодзе ёсць таксама адна з тых, якія лёгкае працягваюць заўважыць.

Спадзяймося, ваша сторанка патрэбуе трох: данныяў корыстніка, паведамленняў і статыстыкі.

Тыповая першая спроба выглядае так:

const users = await getUsers();
const notifications = await getNotifications();
const analytics = await getAnalytics();
Each request waits for the previous one.

Кожны await блакуе, пакуль пытка даўнае рашэння, таму запыткі выконваюцца абоўязкова па рэчы.

Якшо кожная пытка займае працэсама 500 мс, такія последовныя запыткі можу прынесці аж 1,5 секунды чакання.

Якшо ніякая з эых пытак на самай працэсе не завісіць ад іншых, нема прычыны чакаць.

Самэ гэтага і служыць Promise.all():

const [users, notifications, analytics] = await Promise.all([
getUsers(),
getNotifications(),
getAnalytics(),
]);

Усі тры запиты тепер выконваюцца адразу, замест таго каб выконваліся по черзе.

Для панелей керування чырага экрана, якія завантажаюць калькольныя незалежныя джерела дадзейнаў, гэта можа значна скорачыць час завантажэння.

Важны момент

Promise.all() выконваецца быстра: як толькі хоць адна з обявлень не выйшла, весь групавы запит таксама не выйшоў.

Гэта нормальна, калі кожны запит є обавязковым, але якшо частка дадзейнаў є необавязковай, вам патрэбны болей гнучкія падходы.

2. Іспользуйце Promise.allSettled(), калі частковая нявыплата дазволена

Уявіце сабе панель керування адміністратара, якая паказвае:

  • Дохады
  • Пользоватары
  • Упавядомленні
  • Стан системы

Якшо служба упавядомлення раптам перастане працаваць, чы хоць экран увесь павінен стаць порожнім?

Зазвычай нятак — втрата аднаго разделу не павінна зачыніць рэшту сторанкі.

Promise.allSettled() рашае самэ гэтая проблема.

const results = await Promise.allSettled([
getRevenue(),
getUsers(),
getNotifications(),
getSystemHealth(),
]);
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("Success:", result.value);
} else {
console.error("Failed:", result.reason);
}
});

Адзін неудачны вызов больш не знишчае успешных рэзультатаў, якія знаходзяцца ля ўсёга.

Гэты падход ёсць корыстным на панелях керавання, экранах аналізу та інструментах монітарынгу, дзе показ частковых дадзэнняў кращы, чым ўсё жаднае.

3. Даўніце таймаут пры Promise

Раней чы пазней вы ступіце на гэты сценарый: што будзе, якшо запит проста ніколі не вернёцца?

Без захоўніка ваш інтерфейс можа застацца у стане завішання на неакранное час.

Вы можете стварыць маленькі, перадаўальны кэйс, який выкарыстоўвае таймаут:

function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Operation timed out"));
}, ms);
});
return Promise.race([promise, timeout]);
}

Потым выкарыстоўваеце яго там, дзе вы робіце запыт:

try {
const data = await withTimeout(fetch("/api/data"), 5000);
console.log(data);
} catch (error) {
console.error(error);
}

Якшо вызов не завершыцца за пяць секунд, абгорнуты Promise сам адхіляецца.

Гэта значна лепшы досвід, чым заставляць корыстнікаў старайцца разгледзець індыкатор, які ніколи не завершваецца.

4. Перапрыткі неудачных аперацыяў

Сеткі іноды перываюць з’яўленне звязку.

Серверы іноды выканаюць паўзы.

API-і трэціх сторон адчаса працуюць некарэктна.

Усе гэта не павінна прымусова значыць, што корыстнік павінен негайна адразу бачыць паведамленне пра адказку.

Для неудач, якія, верагатна, ўзаемна тымчасовыя, невеликі функцыянт для перапрыткаў дапамагае значна.

async function retry(fn, attempts = 3) {
let lastError;
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
}
}
throw lastError;
}

Вы моглі б вызваць гэта так:

const data = await retry(
() => fetch("/api/data"),
3
);

Аперацыя тепер атрымлеўка калькі дадатковых шансоў, прычаму яе не будзе считаць справжней неудачай.

Але не перапрыткаваць усё слепа.

Статус 500 часта сігналізуе пра тымчасовую проблему сервера.

Статус 401 Unauthorized, з іншай боку, не будзе выправлены праз тры разы зноў апрацоўкі той самай запросу.

Розумная логіка перапрыткаў разлічвае неудачы, якія варта перапрыткаць, і тые, якія — ні.

5. Дадаць адлегліванні межы перапрыткамі

Прагненне запускаць павторыякшчэнне адразу пасля неудачы запита не завжды ўзгодна.

Разглянем сервер, які вже мае труднасці пад высокай навантажэнням.

Якщо тыясчы кліентаў павторуюць запиты адразу, вы ствараеце ўсё большую навантажэння на систему, якая вже перадташчавана.

Простая функцыя з адлегчэнням рашае гэты проблема:

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

Яе можна без проблем адключыць у цыкл павторыякшчэння:

async function retry(fn, attempts = 3) {
let lastError;
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (i < attempts - 1) {
await delay(1000);
}}}
throw lastError;
}

У прыменных системах часта выкорыстоўваецца експансіяванае адлегчэння, калі павторыякшчэнні адбываюцца з павеклікаючымі інтэрваламі:

1 second
2 seconds
4 seconds
8 seconds

Такой спосаб павторыякшчэння дае серверу можлівасць восстанавіцца, замест таго каб спрычыніць шторм павторыякшчэнняў.

6. Контроль канкурантнасці

Існуе проблема, яку Promise.all() можа таямна стварыць.

Спадзяймося, вам трэба обрабаваць 1 000 запитоў API.

Просты падход здаецца дасканальным:

await Promise.all(
items.map((item) => processItem(item))
);
But now you’re potentially starting 1,000 operations at once.

Але гэта значыць, што вы можете запускать всі 1 000 операцыяў роўнасабой у той самы момент.

Этага рэдка калі насправды хочаце.

Часта вы прывяжаны лімітуваць колькість операцыяў, якія выкананы паралельна.

Уявіце ўсё такое:

1000 tasks
↓
5 at a time
↓
5 at a time
↓
5 at a time

Стварыць просты лімітэр паралельнасці не ўскладнена:

async function runWithLimit(items, limit, fn) {
const results = [];
let index = 0;
async function worker() {
while (index < items.length) {
const currentIndex = index++;
results[currentIndex] = await fn(items[currentIndex]);
}
}
const workers = Array.from(
{ length: limit },
() => worker()
);
await Promise.all(workers);
return results;
}

Вы будете яго викорыстоўваць так:

const results = await runWithLimit(
items,
5,
(item) => processItem(item)
);

Тепер вы самі адлучваеце, колькі задач выканае адразу, замест таго каб прыглядаў за гэтым сэрвіс.

Гэтая тэхніка даўляйна практычная, калі вы працуеце з вялікіми набрамі дадзенаў, зовнішнімі API, обробкай файлаў або чергамі фонавых задач.

7. Запобежэнне дублікатным запытам

Это проблема, якая з’яўляецца частае, чым можна было б спадзявацца.

Пользователь завантажае сторунку панелі керування.

Тры разлучныя складовы кожная патрабуюць тых сабе дадзенаў прафіля пользователя.

У замест на тое, каб адправіць тры разлічныя запыткі:

Component A → /api/user
Component B → /api/user
Component C → /api/user

вы можете заставіць іх раздзяляцца адной-ўсёй Promise.

const pendingRequests = new Map();
function getUser(id) {
if (pendingRequests.has(id)) {
return pendingRequests.get(id);
}
const request = fetch(`/api/users/${id}`)
.then((res) => res.json())
.finally(() => {
pendingRequests.delete(id);
});
pendingRequests.set(id, request);
return request;
}

З такім падходам, якшо тры компаненты запрашваюць таго ж паўтаральніка прыблізна ў той жа момент, яны всі прыўяжуцца да аднаго запытку, які ўжо ў парадку выпалення, у замест на тое, каб стварыць тры запыткі.

Іншымі словамі:

3 запыткі → 1 запытка

Гэтыя методы часта называюцься удаленнем дуплікацыі запыткаў.

Для большых кодавых базаў інструменты, такія як TanStack Query, вже реалізуюць логіку кэшавання і удалення дуплікацыі саме так.

8. Іспользуйце Promise.any(), калі вам патрэбен толькі адны успешны результат

Інодзе тая ж сама інформацыя є доступная з кальколькі разлічных месцаў.

Напрыклад:

API Server A
API Server B
API Server C

Якшо ваша аплікацыя патрэбуе толькі аднаго успешнага адпаведзення з якога-небудзь з іх, Promise.any() падходіць для гэтай задачы.

const result = await Promise.any([
fetchFromServerA(),
fetchFromServerB(),
fetchFromServerC(),
]);

Той Promise, які выпаняецца першым, выграеў гонку.

Заазначыце, што ўжоцьве стаўленне да гэтага функцыяналу не адпаведае Promise.race().

Promise.race() рашуе або адхіляе запит на падставе таго Promise, які заканчываецца першым, незалежна ад таго, чы то успех, чы неудача.

Promise.any() замест таго чакае самэўсёльва на першы Promise, які выпаняецца з успехам, ігнаруючы адхіленняя, якщо толькі не всі з іх зазнаюць неудачы.

Гэтае разлікаванне можа здавацца незначным, але яно можа цалкам зменіць спосаб, якім вы карыстаецеся для адпрацоўкі памылак.

9. Анулюйце работу, якая вам больш не патрэбна

Гэты патэран ўваходзіць у числе найболей задовольняючых для застосоўкі.

Падумайце пра полье для жываго пошуку:

user types: react
user types: react dashboard
user types: react dashboard ui

Вероятна, вы не хачаце, каб запиты на кожны пакульні натиск клавішы продалей працавала на фоне, калі воны стануць застарэлымі.

Гэта самэйша ситуацыя, для якой быў створаны AbortController.

const controller = new AbortController();
fetch("/api/search?q=react", {
signal: controller.signal,
});

Калі запит больш не патрэбны, проста вызываеце:

controller.abort();
The request can then be cancelled.

Гэты патэрн часта з’яўляецца ў такіх сцэнарыях, як:

  • Прамовы для пошуку
  • Поле автодаполнення
  • Пераходы між маршрутамі або сторанкамі
  • Зніманне або чыстка компонентаў
  • Запиты, якія заменяюцца новымі

Настоямай спэцыялізацыёй тут не ёсьць проста вызыванне abort().

Это ўмець распазнаваць тады, калі поперадня работа больш не ўжо корыстная.

Большая наука

Обяцанні (Promises) не здаюцца складнымі таму, што .then() — гэта якаясь сложная API.

Яны здаюцца складнымі таму, што рэальныя прыклады застосоў уключаюць заплутану, мнагашаровую асінхронную працę.

Вам постаўляецца неабходнасць постоянна рашаць такія пытанні, як:

Чы гэтыя операцыі павінны выконвацца адночасова?

Какі план, якщо адна з іх не выйдзе?

Скількі часу ўважаецца занадта довгім чаканнем?

Чы варта тут прабаваць знову?

Скількі операцый павінны выконвацца паралельна?

Магчыма ліквідаваць падвойную роботу, яка ўжо трывае?

Чы гэтае запитванне ўсё яшчэ мае значэнне?

Калі вы почынаеце ставіць такія запитанні, Promises перестаюць быць проста функцыяй мовы.

Яны станоўляць сабой справжній архітектурны інструмент.

Мае 9 шаблонаў Promises у кароткаму апісе

Promise.all() — гэта найкращы спосаб для адзіночнага запуску незалежных заданняў. Promise.allSettled() адмаўляеся з частымі неудачамі. Promise.race() падходзіць для прынуджання да завершэння за час або реагавання на тое, што завершыцца першае. Логіка павторных спроб дапамагае восстанавіцца пасля тымчасовых неудач. Стратэгіі з адкладаннем і павторнымі спробамі запобiegаюць занадто агрэсыўным павторным спробам. Обмежэнне канкурантнасці дапамагае контролаваць вялікія завантажэння. Усуненне дуплікацыя запытак стоіць за тым, каб не было дуплікатных вызоў API. Promise.any() вяртае першы успешны рэзультат сярод калькі. AbortController дазволяе анулюваць работу, якая больш не ўпэўнена.

У кожным проекте вам не патрэбны ўсі этыя патэрыны.

Фактчына, ўжыванне ўсіх іх было б контрапродуктываўнае.

Працоўны навык заключаецца ў тым, каб з’ясаваць, якую проблему вы рашыце, перш чым вярнуцца да конкрэтнага патэрына.

Заключныя меркіі

Наймоцнейшы код асінхроннага типу — это не той, які заполнены найхитрэйшымі трюкамі з Promise.

Это код, у якому обработка адзінакоў, таймінг, канкурэнцыя і анулювання былі розглянуты ўважна, прычым яны не ператварыліся на проблемы ў рэальных умовах.

Пачніце з самай простой версіі.

Зверніце увагу на тое, што насправды вызывае проблемы.

Ляч тады застосавіце конкрэтны патэран, який рашае гэтыя проблемы.

Адтуды, ў дзеякіх случаях, найпрыгожэйшы крок у JavaScript — это усвядомленне тады, калі патэран узагалі не трэба.

Спадневаная літэратура

  • Выбір между Promise.all, Promise.race і парадигмай Sequential Awaits — Дзеянае пра тое, калі Promise.all() прышвычвае API Node.js, чаму ён шырока падае при будзь-яй адхіленні, і рамкі для прыняцтва рашэння па выбору правильнага асінхроннага патэрану.
  • Async/Await протыку Promises: Чаму насправдзе розлічаюцца ўнутранні механізмы — Пасвячана рэальным разлікам у выконаванні, выкарыстоўванні памяці і стэк-трэйсе между async/await і Promises, а таксама прычынам, чаму інодзе трэба вяртацца да базовых API Promises.