Artiklen udforsker brugen af Async/Await i JavaScript med konkrete eksempler. Generelt set er Async/Await en praktisk måde at skrive asynkron kode på. Før denne funktion blev indført, blev lignende kode skrevet ved hjælp af callbacks og promises. Forfatteren af den oprindelige artikel fremhæver fordelene ved Async/Await ved at gennemgå forskellige eksempler.
Vi minder om: til alle læsere af 'Habr' — rabat på 10.000 rubler ved tilmelding til et hvilket som helst Skillbox-kursus med rabatkoden 'Habr'.
Skillbox anbefaler: Online uddannelseskursus .
Callback
En callback er en funktion, hvis kald er forsinket i en ubestemt periode. Tidligere blev callbacks anvendt i de dele af koden, hvor resultatet ikke kunne opnås med det samme.
Her er et eksempel på asynkron fillæsning i Node.js:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
}); Problemer opstår, når der skal udføres flere asynkrone operationer samtidig. Lad os forestille os et scenarie: der sendes en forespørgsel til databasen for brugeren Arfat, og hans felt profile_img_url skal hentes, hvorefter billedet skal uploades fra serveren som someserver.com.
Efter uploaden konverteres billedet til et andet format, f.eks. fra PNG til JPEG. Hvis konverteringen var vellykket, sendes en e-mail til brugeren. Derefter registreres informationen om hændelsen i transformations.log-filen med angivelse af dato.
Det er værd at bemærke, at indlejringen af callbacks og det store antal }) i slutningen af koden kan give problemer. Dette kaldes Callback Hell eller Pyramid of Doom.
Ulemperne ved denne tilgang er åbenlyse:
- Denne kode er svær at læse.
- Det er også vanskeligt at håndtere fejl, hvilket ofte fører til en forringelse af kodekvaliteten.
For at løse dette problem blev promises tilføjet i JavaScript. De giver mulighed for at erstatte den dybe indlejring af callbacks med ordet .then.
En positiv ting ved promises er, at koden bliver meget lettere at læse, idet man læser den ovenfra og ned, snarere end fra venstre mod højre. Ikke desto mindre har promises også deres egne problemer:
- Der skal tilføjes mange .then.
- I stedet for try/catch bruges .catch til at håndtere alle fejl.
- Det er langtfra altid praktisk at arbejde med flere promises inden for en enkelt løkke, i nogle tilfælde komplicerer de faktisk koden.
Her er en opgave, der vil illustrere værdien af den sidste pointe.
Lad os antage, at der er en for-løkke, der udskriver en sekvens af tal fra 0 til 10 med et tilfældigt interval (0–n sekunder). Ved at bruge løfter skal denne løkke ændres, så tallene udskrives i rækkefølge fra 0 til 10. Så hvis udskrivningen af nul tager 6 sekunder, og et tager 2 sekunder, så skal nul først udskrives, før tælleren for et begynder.
Og selvfølgelig bruger vi ikke Async/Await eller .sort for at løse denne opgave. Et eksempel på løsningen er i slutningen.
Async-funktioner
Tilføjelsen af async-funktioner i ES2017 (ES8) har gjort det lettere at arbejde med løfter. Jeg vil bemærke, at async-funktioner arbejder "oven på" løfter. Disse funktioner repræsenterer ikke kvalitativt andre koncepter. Async-funktioner blev tænkt som et alternativ til kode, der bruger løfter.
Async/Await giver mulighed for at organisere arbejdet med asynkron kode i en synkron stil.
Derfor letter kendskabet til løfter forståelsen af principperne bag Async/Await.
Syntaks
I en almindelig situation består det af to nøgleord: async og await. Det første ord gør funktionen asynkron. I sådanne funktioner er brugen af await tilladt. Ellers vil brugen af denne funktion forårsage en fejl.
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
Async indsættes i begyndelsen af funktionsdeklarationen, og i tilfælde af brug af en pilfunktion – mellem lighedstegnet "=" og parenteserne.
Disse funktioner kan placeres i et objekt som metoder eller bruges i en klassedeklaration.
// As an object's method
const obj = {
async getName() {
return fetch('https://www.example.com');
}
}
// In a class
class Obj {
async getResource() {
return fetch('https://www.example.com');
}
}NB! Det er værd at huske, at konstruktører i klassen og gettere/settere ikke kan være asynkrone.
Semantikken og reglerne for udførelse
Async-funktioner ligner i princippet standard JS-funktioner, men der er undtagelser.
Så async-funktioner returnerer altid løfter:
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloSpecifikt returnerer fn strengen hello. Og da dette er en asynkron funktion, pakkes værdien af strengen ind i et løfte ved hjælp af konstruktøren.
Her er en alternativ konstruktion uden Async:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// helloI dette tilfælde returneres løftet "manuelt". Den asynkrone funktion pakkes altid ind i et nyt løfte.
Hvis den returnerede værdi er en primitiv, udfører async-funktionen en værdiretur ved at pakke den ind i et løfte. Hvis den returnerede værdi derimod er et løfteobjekt, returneres dets opfyldelse i et nyt løfte.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Men hvad sker der, hvis der opstår en fejl inde i den asynkrone funktion?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);Hvis den ikke håndteres, vil foo() returnere et løfte med en afvisning. I denne situation vil Promise.reject, som indeholder fejlen, blive returneret i stedet for Promise.resolve.
Async-funktioner giver altid et løfte som output, uanset hvad der returneres.
Asynkrone funktioner pauses ved hvert await.
Await påvirker udtryk. Hvis udtrykket er et løfte, pauses async-funktionen indtil løftet er opfyldt. Hvis udtrykket ikke er et løfte, konverteres det til et løfte gennem Promise.resolve og afsluttes derefter.
// utility function to cause delay
// and get random value
const delayAndGetRandom = (ms) => {
return new Promise(resolve => setTimeout(
() => {
const val = Math.trunc(Math.random() * 100);
resolve(val);
}, ms
));
};
async function fn() {
const a = await 9;
const b = await delayAndGetRandom(1000);
const c = await 5;
await delayAndGetRandom(1000);
return a + b * c;
}
// Execute fn
fn().then(console.log);Her er en beskrivelse af, hvordan fn-funktionen fungerer.
- Efter kaldet bliver den første linje konverteret fra const a = await 9; til const a = await Promise.resolve(9);.
- Efter brugen af Await pauses funktionen, indtil a får sin værdi (i dette tilfælde 9).
- delayAndGetRandom(1000) pauser udførelsen af fn-funktionen, indtil den er færdig (efter 1 sekund). Dette er i bund og grund en stop af fn-funktionen i 1 sekund.
- delayAndGetRandom(1000) returnerer et tilfældigt værdi gennem resolve, som derefter tildeles variablen b.
- Og tilfælde med variablen c er analogt med tilfælde med variablen a. Derefter stopper alt i et sekund, men nu returnerer delayAndGetRandom(1000) ingenting, fordi det ikke er nødvendigt.
- Som et resultat beregnes værdierne ved hjælp af formlen a + b * c. Resultatet pakkes ind i et løfte ved hjælp af Promise.resolve og returneres af funktionen.
Disse pauser kan minde om generatorer i ES6, men der er .
Løs opgaven
Lad os nu se på løsningen på den opgave, der blev nævnt ovenfor.
I funktionen finishMyTask anvendes Await til at vente på resultaterne af operationer som queryDatabase, sendEmail, logTaskInFile og andre. Hvis man sammenligner denne løsning med den, hvor der blev anvendt forjættelser, bliver ligheden tydelig. Ikke desto mindre forenkler versionen med Async/Await betydeligt de syntaktiske komplikationer. Der er ikke mange callbacks og kæder som .then/.catch.
Her er en løsning med output af tal, hvor der er to varianter.
const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
let pr = Promise.resolve(0);
for (let i = 1; i <= 10; i += 1) {
pr = pr.then((val) => {
console.log(val);
return wait(i, Math.random() * 1000);
});
}
resolve(pr);
});
// Implementation Two (Using Recursion)
const printNumbersRecursive = () => {
return Promise.resolve(0).then(function processNextPromise(i) {
if (i === 10) {
return undefined;
}
return wait(i, Math.random() * 1000).then((val) => {
console.log(val);
return processNextPromise(i + 1);
});
});
};Og her er løsningen med brug af async-funktioner.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}Error handling
Ubehandlede fejl indkapsles i en rejected forjættelse. I async-funktioner kan man dog bruge try/catch-konstruktionen til at håndtere fejl synkront.
async function canRejectOrReturn() {
// vent et sekund
await new Promise(res => setTimeout(res, 1000));
// Forkast med ~50% sandsynlighed
if (Math.random() > 0.5) {
throw new Error('Beklager, tallet er for stort.')
}
return 'perfekt tal';
}canRejectOrReturn() er en asynkron funktion, der enten gennemføres succesfuldt ("perfekt tal") eller fejler med en fejl ("Beklager, tallet er for stort").
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'fejl fanget';
}
}Da der i ovenstående eksempel forventes en udførelse af canRejectOrReturn, vil en fejl i den selv medføre, at catch-blokken udføres. Resultatet er, at funktionen foo enten afsluttes med undefined (når der ikke returneres noget i try-blokken) eller med 'fejl fanget'. Derfor vil denne funktion ikke mislykkes, da try/catch tager sig af selve funktionen foo.
Her er et andet eksempel:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'fejl fanget';
}
}Det er værd at bemærke, at der i eksemplet fra foo returneres canRejectOrReturn. Foo afsluttes i dette tilfælde enten med 'perfekt tal' eller med fejlen Error ("Beklager, tallet er for stort"). Catch-blokken vil aldrig blive udført.
Problemet er, at foo returnerer den forjættelse, der er givet fra canRejectOrReturn. Derfor bliver løsningen for funktionen foo løsningen for canRejectOrReturn. I dette tilfælde vil koden kun bestå af to linjer:
try {
const promise = canRejectOrReturn();
return promise;
}Hvad vil der ske, hvis man bruger både await og return sammen:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'fejl fanget';
}
}I koden ovenfor vil foo afsluttes med både 'perfekt tal' og 'fejl fanget'. Der vil ikke være nogen afvisninger. Men foo vil afsluttes med canRejectOrReturn, ikke med undefined. Lad os sikre os dette ved at fjerne linjen return await canRejectOrReturn():
try {
const value = await canRejectOrReturn();
return value;
}
// …Almindelige fejl og faldgruber
I nogle tilfælde kan brugen af Async/Await føre til fejl.
Glemt await
Det sker ret ofte — nøgleordet await glemmes foran promissen:
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}I koden, som man kan se, er der hverken await eller return. Derfor afsluttes foo altid med undefined uden en forsinkelse på 1 sekund. Men promissen vil blive udført. Hvis den derimod giver en fejl eller bliver afvist, vil der i dette tilfælde blive udløst UnhandledPromiseRejectionWarning.
Async-funktioner i callback
Async-funktioner bruges ofte i .map eller .filter som callbacks. Som eksempel kan vi tage funktionen fetchPublicReposCount(username), der returnerer antallet af åbne GitHub-repositorier. Antag, at der er tre brugere, hvis data vi har brug for. Her er koden til denne opgave:
const url = 'https://api.github.com/users';
// Nyttig fn til at hente repo counts
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}Vi har brug for kontiene ArfatSalman, octocat, norvig. I dette tilfælde udfører vi:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});Det er værd at bemærke Await i callbacken .map. Her er counts et array af promisser, og .map er en anonym callback for hver af de angivne brugere.
Overdreven sekventiel brug af await
Som eksempel kan vi tage følgende kode:
async function fetchAllCounts(users) {
const counts = [];
for (let i = 0; i < users.length; i++) {
const username = users[i];
const count = await fetchPublicReposCount(username);
counts.push(count);
}
return counts;
}Her placeres antallet af repos i variablen count, hvorefter dette tal tilføjes til arrayet counts. Problemet med koden er, at indtil der kommer data fra serveren for den første bruger, vil alle efterfølgende brugere forblive i ventetilstand. Således bliver kun én bruger behandlet ad gangen.
Hvis det f.eks. tager omkring 300 ms at behandle én bruger, så er det allerede en sekund for alle brugere; den tid, der bruges, er lineært afhængig af antallet af brugere. Men da hentningen af antallet af repos ikke afhænger af hinanden, kan processerne paralleliseres. For dette skal vi arbejde med .map og Promise.all:
async function fetchAllCounts(users) {
const promises = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
return Promise.all(promises);
}Promise.all tager et array af promisser som input. Det afslutter med det sidste, når alle promisser i arrayet er fuldført eller ved den første afvisning. Det kan ske, at de ikke alle starter samtidigt — for at sikre samtidig opstart kan man bruge p-map.
Konklusion
Async-funktioner bliver stadig mere vigtige for udviklingen. For adaptiv brug af async-funktioner er det en god idé at benytte . En JavaScript-udvikler skal have et solidt kendskab til dette.
Skillbox anbefaler:
- Praktisk kursus .
- Anvendt online-kursus .
- Toårigt praktisk kursus .
Kilde: habr.com
