JavaScript async/await: asinchroninio kodo valdymas

Kodėl asinchroninis kodas tapo būtinybe

Prisimenu laikus, kai JavaScript buvo paprastas skriptų rinkinys, skirtas formos laukų validacijai ir mygtukų spalvų keitimui. Dabar tai viena galingiausių programavimo kalbų, kuria kuriami sudėtingi serveriai, mobiliosios aplikacijos ir net dirbtinio intelekto modeliai. Bet kartu su galimybėmis atėjo ir sudėtingumas – ypač kai reikia valdyti asinchronines operacijas.

Kiekvienas programuotojas, dirbęs su JavaScript, susidūrė su situacija, kai reikia palaukti, kol duomenys atkeliaus iš serverio, failas bus nuskaitytas arba vartotojas patvirtins kokį nors veiksmą. Sinchroninis kodas tokiose situacijose tiesiog sustabdytų visą programą – naršyklė užšaltų, vartotojas nervintųsi, o jūs gautumėte piktų el. laiškų.

Asinchroninis kodas leidžia programai toliau veikti, kol laukiama rezultato. Bet kaip tai valdyti, kad kodas liktų skaitomas ir nesivirstų chaotiška callback’ų makaronų krūva? Čia ir ateina į pagalbą async/await sintaksė.

Nuo callback pragarų iki Promise žemės

Prieš pasinerdami į async/await, verta suprasti, kokią problemą jie sprendžia. Anksčiau asinchroniniam kodui valdyti naudojome callback funkcijas. Atrodė paprasta: perduodi funkciją, kuri bus iškviesta, kai operacija baigsis. Teoriškai puiku, praktiškai – košmaras.

„`javascript
getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
getMoreData(c, function(d) {
// Sveiki atvykę į callback pragarą
});
});
});
});
„`

Šis „callback hell” arba „pyramid of doom” tapo legendiniu anti-pattern’u. Kodas darosi nesuprantamas, klaidų valdymas virsta košmaru, o refaktoringas – beviltiška užduotimi.

Tada atsirado Promise objektai. Jie leido rašyti grandinėles su .then(), kas buvo žymiai geriau, bet vis tiek ne idealas:

„`javascript
getData()
.then(a => getMoreData(a))
.then(b => getMoreData(b))
.then(c => getMoreData(c))
.then(d => {
// Geriau, bet vis dar ne tobula
})
.catch(error => console.error(error));
„`

Async/await magija paprastais žodžiais

Pagaliau 2017 metais su ES2017 (ES8) standarto atėjimu gavome async/await sintaksę. Tai iš esmės yra „cukrus” ant Promise – jie veikia su tais pačiais Promise objektais, bet leidžia rašyti asinchroninį kodą taip, lyg jis būtų sinchroninis.

Pagrindinė idėja paprasta: funkcija, pažymėta žodžiu async, visada grąžina Promise. O žodelis await sustabdo funkcijos vykdymą, kol Promise bus įvykdytas (resolved arba rejected).

„`javascript
async function fetchUserData() {
const response = await fetch(‘https://api.example.com/user’);
const data = await response.json();
return data;
}
„`

Pažiūrėkite, kaip skaitoma! Nėra callback’ų, nėra .then() grandinėlių – tiesiog eilutė po eilutės, kaip įprastas sinchroninis kodas. Bet po gaubtu vis dar veikia asinchroninis mechanizmas, kuris neleidžia užšalti programai.

Kaip tai veikia praktikoje

Kai pažymite funkciją žodžiu async, ji automatiškai tampa funkcija, kuri grąžina Promise. Net jei tiesiog grąžinate paprastą reikšmę, ji bus įvyniota į Promise:

„`javascript
async function getNumber() {
return 42;
}

// Tas pats kaip:
function getNumber() {
return Promise.resolve(42);
}
„`

Žodelis await gali būti naudojamas tik async funkcijų viduje (nors naujose JavaScript versijose galima naudoti ir top-level, bet apie tai vėliau). Kai JavaScript interpreteris pasiekia await, jis:

1. Pristabdo funkcijos vykdymą toje vietoje
2. Leidžia vykdyti kitą kodą (neblokuoja)
3. Kai Promise įvykdomas, tęsia funkcijos vykdymą nuo tos vietos
4. Jei Promise sėkmingas, grąžina rezultatą
5. Jei Promise atmestas, meta klaidą

Štai realus pavyzdys, kaip galėtumėte naudoti tai kuriant aplikaciją:

„`javascript
async function loadUserProfile(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchUserPosts(userId);
const friends = await fetchUserFriends(userId);

return {
user,
posts,
friends
};
} catch (error) {
console.error(‘Nepavyko užkrauti profilio:’, error);
throw error;
}
}
„`

Klaidų valdymas be galvos skausmo

Vienas didžiausių async/await privalumų – intuityvus klaidų valdymas naudojant įprastą try/catch bloką. Su Promise ir .then() turėjote naudoti .catch(), o su callback’ais – perduoti error kaip pirmą parametrą. Su async/await viskas vienoda:

„`javascript
async function saveData(data) {
try {
await validateData(data);
await saveToDatabase(data);
await sendNotification(‘Duomenys išsaugoti’);
return { success: true };
} catch (error) {
if (error.type === ‘ValidationError’) {
return { success: false, message: ‘Neteisingi duomenys’ };
}
if (error.type === ‘DatabaseError’) {
return { success: false, message: ‘Nepavyko išsaugoti’ };
}
throw error; // Kitas klaidas perduodam aukščiau
}
}
„`

Galite valdyti skirtingus klaidų tipus, naudoti kelis catch blokus, pridėti finally – viskas veikia taip, kaip tikitės iš sinchroninio kodo.

Lygiagretus vykdymas su Promise.all()

Viena dažna klaida, kurią mačiau dešimtis kartų: programuotojai naudoja await nuosekliai, nors operacijos galėtų vykti lygiagrečiai. Pažiūrėkite į šį kodą:

„`javascript
async function loadData() {
const users = await fetchUsers(); // Laukia 2 sekundes
const products = await fetchProducts(); // Laukia 3 sekundes
const orders = await fetchOrders(); // Laukia 2 sekundes
// Iš viso: 7 sekundės
}
„`

Problema ta, kad šios operacijos nepriklauso viena nuo kitos – jos gali vykti vienu metu! Teisingas būdas:

„`javascript
async function loadData() {
const [users, products, orders] = await Promise.all([
fetchUsers(), // Vyksta lygiagrečiai
fetchProducts(), // Vyksta lygiagrečiai
fetchOrders() // Vyksta lygiagrečiai
]);
// Iš viso: ~3 sekundės (ilgiausia operacija)
}
„`

Promise.all() priima Promise masyvą ir grąžina naują Promise, kuris įvykdomas, kai visi masyvo Promise įvykdomi. Jei bent vienas Promise atmestas, visas Promise.all() bus atmestas.

Jei norite, kad programa tęstųsi net jei kai kurie Promise atmesti, naudokite Promise.allSettled():

„`javascript
async function loadOptionalData() {
const results = await Promise.allSettled([
fetchCriticalData(),
fetchOptionalData1(),
fetchOptionalData2()
]);

results.forEach((result, index) => {
if (result.status === ‘fulfilled’) {
console.log(`Operacija ${index} sėkminga:`, result.value);
} else {
console.log(`Operacija ${index} nepavyko:`, result.reason);
}
});
}
„`

Dažniausios klaidos ir kaip jų išvengti

Pamirštas await

Tai klasika. Parašote async funkciją, bet pamiršote await:

„`javascript
async function getData() {
const data = fetchData(); // Pamirštas await!
console.log(data); // Išspausdins Promise objektą, ne duomenis
}
„`

Kai kurios IDE ir linteriai praneša apie tokias klaidas, bet ne visada. Būkite budrūs.

Await cikluose

Naudojant await paprastame for cikle, operacijos vykdys nuosekliai:

„`javascript
// Lėtas būdas
for (let id of userIds) {
const user = await fetchUser(id); // Kiekvienas laukia eilės
users.push(user);
}

// Greitas būdas
const userPromises = userIds.map(id => fetchUser(id));
const users = await Promise.all(userPromises);
„`

Top-level await

Senesnėse JavaScript versijose negalėjote naudoti await už funkcijos ribų. Dabar su ES2022 moduliuose galite:

„`javascript
// module.js
const data = await fetchData();
export default data;
„`

Bet atsargiai – tai blokuoja viso modulio įkėlimą, kol Promise įvykdomas. Naudokite tik kai tikrai reikia.

Praktiniai patarimai realiam gyvenimui

Timeout’ai ir retry mechanizmai

Dažnai reikia apriboti, kiek laiko lauksite atsakymo. Štai paprasta timeout funkcija:

„`javascript
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error(‘Timeout’)), ms)
);
}

async function fetchWithTimeout(url, ms = 5000) {
try {
return await Promise.race([
fetch(url),
timeout(ms)
]);
} catch (error) {
console.error(‘Užklausa užtruko per ilgai arba nepavyko’);
throw error;
}
}
„`

Retry mechanizmas, kai operacija gali nepavykti:

„`javascript
async function retryOperation(operation, maxRetries = 3, delay = 1000) {
for (let i = 0; i < maxRetries; i++) { try { return await operation(); } catch (error) { if (i === maxRetries - 1) throw error; console.log(`Bandymas ${i + 1} nepavyko, bandome dar kartą...`); await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

// Naudojimas
const data = await retryOperation(() => fetchData(), 3, 2000);
„`

Debouncing ir throttling

Kai dirbate su vartotojo įvestimi, dažnai reikia apriboti, kaip dažnai vykdoma operacija:

„`javascript
function debounce(func, wait) {
let timeout;
return function executedFunction(…args) {
const later = () => {
clearTimeout(timeout);
func(…args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}

const debouncedSearch = debounce(async (query) => {
const results = await searchAPI(query);
displayResults(results);
}, 300);
„`

Progreso sekimas

Kai vykdote daug operacijų, naudinga rodyti progresą:

„`javascript
async function processItemsWithProgress(items) {
let completed = 0;
const total = items.length;

const promises = items.map(async (item) => {
const result = await processItem(item);
completed++;
updateProgressBar(completed / total * 100);
return result;
});

return await Promise.all(promises);
}
„`

Kada async/await yra per daug

Nors async/await yra puikus įrankis, ne visada jis yra geriausias pasirinkimas. Kartais paprastas .then() yra aiškesnis, ypač kai turite paprastą grandinėlę be sudėtingos logikos:

„`javascript
// Kartais tai yra OK
fetchData()
.then(data => processData(data))
.then(result => saveResult(result))
.catch(handleError);
„`

Taip pat, jei kuriate biblioteką ar API, kuris grąžina Promise, nebūtina visko vynioti į async funkcijas. Promise yra gerai dokumentuotas standartas, ir kiti programuotojai žino, kaip su jais dirbti.

Dar vienas aspektas – našumas. Nors skirtumas dažniausiai nereikšmingas, async/await sukuria papildomą Promise įvyniojimu lygį. Kritinėse našumo vietose (pavyzdžiui, bibliotekose, kurios vykdomos milijonus kartų per sekundę) tai gali turėti įtakos.

Ateitis jau čia: kas toliau?

JavaScript ekosistema nuolat vystosi. Async iteratoriai ir generatoriai leidžia dirbti su asinchroniniais duomenų srautais elegantiškai:

„`javascript
async function* fetchPages(url) {
let page = 1;
while (true) {
const data = await fetch(`${url}?page=${page}`);
if (data.items.length === 0) break;
yield data.items;
page++;
}
}

// Naudojimas
for await (const items of fetchPages(‘/api/items’)) {
processItems(items);
}
„`

Top-level await moduliuose tampa standartu, o naujos funkcijos kaip Promise.any() ir Promise.race() suteikia dar daugiau galimybių valdyti asinchronines operacijas.

Realybėje async/await tapo neatsiejama šiuolaikinio JavaScript programavimo dalimi. Beveik kiekviena biblioteka, framework’as ar API juos naudoja. React Hooks, Node.js serveriai, duomenų bazių ORM’ai – visur rasite šią sintaksę.

Geriausias patarimas, kurį galiu duoti: rašykite daug kodo. Eksperimentuokite su skirtingais scenarijais, darykite klaidas, mokykitės iš jų. Skaitykite kitų žmonių kodą – GitHub pilnas puikių pavyzdžių. Ir svarbiausia – nesistenkite viską padaryti per daug sudėtingai. Kartais paprasčiausias sprendimas yra geriausias.

Asinchroninis programavimas iš pradžių gali atrodyti bauginantis, bet su async/await jis tampa valdomas ir net malonus. Tai įrankis, kuris leidžia kurti greitesnes, atsakingesnes ir vartotojui draugiškesnes aplikacijas. O ar ne tam mes visi čia ir esame?

Daugiau

Dapr: paskirstytas aplikacijų vykdymo laikas