Artikelns författare gÄr igenom exempel pÄ Async/Await i JavaScript. Generellt sett Àr Async/Await ett bekvÀmt sÀtt att skriva asynkron kod. Innan denna funktionalitet fanns skrivs sÄdan kod med hjÀlp av callbacks och promiser. Författaren av den ursprungliga artikeln belyser fördelarna med Async/Await genom att gÄ igenom olika exempel.
PĂ„minnelse: för alla lĂ€sare av "Habr" â 10 000 rubel rabatt vid registrering för vilken kurs som helst pĂ„ Skillbox med rabattkoden "Habr".
Skillbox rekommenderar: Utbildningskurs online .
Callback
En callback Àr en funktion vars anrop skjuts upp pÄ obestÀmd tid. Tidigare anvÀndes callbacks i de delar av koden dÀr resultatet inte kunde erhÄllas omedelbart.
HÀr Àr ett exempel pÄ asynkront fillÀsande i Node.js:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
}); Problem uppstÄr nÀr flera asynkrona operationer mÄste utföras samtidigt. LÄt oss förestÀlla oss följande scenario: en databasfrÄga görs för anvÀndaren Arfat, dÀr vi behöver lÀsa hans fÀlt profile_img_url och ladda ner bilden frÄn servern som ligger pÄ someserver.com.
Efter nedladdningen konverterar vi bilden till ett annat format, till exempel frÄn PNG till JPEG. Om konverteringen lyckas skickas ett e-postmeddelande till anvÀndaren. Informationen om hÀndelsen registreras sedan i transformations.log-filen med datum.
Det Àr vÀrt att notera hur callbacks Àr inbÀddade och hur mÄnga }) som finns i slutet av koden. Det kallas Callback Hell eller Pyramid of Doom.
Nackdelarna med detta tillvÀgagÄngssÀtt Àr uppenbara:
- Koden Àr svÄr att lÀsa.
- Det Àr ocksÄ svÄrt att hantera fel, vilket ofta leder till en försÀmring av kodkvaliteten.
För att lösa detta problem har promiser lagts till i JavaScript. De gör det möjligt att ersÀtta djup inbÀddning av callbacks med .then.
En positiv aspekt av promiser Àr att koden blivit mycket lÀttare att lÀsa, frÄn toppen och ner snarare Àn frÄn vÀnster till höger. Trots det har promiser ocksÄ sina egna problem:
- Det krÀvs en stor mÀngd .then.
- IstÀllet för try/catch anvÀnds .catch för att hantera alla fel.
- Att arbeta med flera promiser inom samma cykel Àr lÄngt ifrÄn alltid bekvÀmt; i vissa fall komplicerar de koden.
HÀr Àr en uppgift som visar vÀrdet av den senaste punkten.
Anta att det finns en for-loop som skriver ut en sekvens av siffror frĂ„n 0 till 10 med en slumpmĂ€ssig paus (0ân sekunder). Genom att anvĂ€nda löften behöver vi Ă€ndra denna loop sĂ„ att siffrorna skrivs ut i sekvens frĂ„n 0 till 10. Om det tar 6 sekunder att skriva ut noll och 2 sekunder att skriva ut ett, sĂ„ bör noll skrivas ut först och sedan ska nedrĂ€kningen av ett börja.
Och sjÀlvklart anvÀnder vi inte Async/Await eller .sort för att lösa denna uppgift. Exempel pÄ lösning kommer i slutet.
Async-funktioner
TillÀgget av async-funktioner i ES2017 (ES8) har underlÀttat arbetet med löften. Jag vill understryka att async-funktioner fungerar "ovanpÄ" löften. Dessa funktioner representerar inte en kvalitativt annan koncept. Async-funktioner var tÀnkt som ett alternativ till kod som anvÀnder löften.
Async/Await ger möjlighet att organisera arbete med asynkron kod i en synkron stil.
DÀrmed underlÀttar kunskap om löften förstÄelsen av principerna för Async/Await.
Syntax
I vanliga fall bestÄr den av tvÄ nyckelord: async och await. Det första ordet gör funktionen asynkron. I sÄdana funktioner tillÄts anvÀndning av await. I alla andra fall kommer anvÀndning av denna funktion att orsaka ett fel.
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
Async placeras i början av funktionsdeklarationen, och i fallet med en arrow-funktion placeras den mellan "=" och parenteserna.
Dessa funktioner kan placeras i ett objekt som metoder eller anvÀndas i en klassdeklaration.
// 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 Àr viktigt att komma ihÄg att klasskonstruktorer och getters/setters inte kan vara asynkrona.
Semantik och körningsregler
Async-funktioner liknar i princip standard JS-funktioner, men det finns undantag.
SÄledes returnerar async-funktioner alltid löften:
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloSpecifikt returnerar fn strÀngen hello. Eftersom detta Àr en asynkron funktion, sÄ sveps vÀrdet av strÀngen in i ett löfte med hjÀlp av konstruktorn.
HÀr Àr en alternativ konstruktion utan Async:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// helloI detta fall utförs Äterkomsten av löftet "manuellt". En asynkron funktion sveps alltid in i ett nytt löfte.
Om det returnerade vÀrdet Àr en primitiv, kommer async-funktionen att returnera vÀrdet inbÀddat i ett löfte. Om det returnerade vÀrdet redan Àr ett löfte, sÄ returneras dess förverkligande i ett nytt löfte.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Men vad hÀnder om det intrÀffar ett fel inuti den asynkrona funktionen?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);Om det inte hanteras, kommer foo() att returnera ett löfte med avvisande. I sÄdana fall kommer istÀllet Promise.resolve att returnera Promise.reject som innehÄller felet.
Async-funktioner ger alltid ett löfte ut, oavsett vad som returneras.
Asynkrona funktioner pausar vid varje await.
Await pÄverkar uttryck. Om uttrycket Àr ett löfte, pausar async-funktionen tills löftet fullgörs. Om uttrycket inte Àr ett löfte, konverteras det till ett löfte via Promise.resolve och fullföljs sedan.
// 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);HÀr Àr en beskrivning av hur fn-funktionen fungerar.
- Efter att den har anropats omvandlas den första raden frÄn const a = await 9; till const a = await Promise.resolve(9);.
- Efter att Await anvÀnds, pausar funktionen medan a fÄr sitt vÀrde (i detta fall 9).
- delayAndGetRandom(1000) pausar exekveringen av fn-funktionen tills den Àr klar (efter 1 sekund). Detta Àr i praktiken en paus av fn-funktionen i 1 sekund.
- delayAndGetRandom(1000) returnerar ett slumptal genom resolve, vilket sedan tilldelas variabeln b.
- Och fallet med variabeln c Àr liknande fallet med variabeln a. Efter det stannar allt i en sekund, men nu returnerar delayAndGetRandom(1000) ingenting eftersom det inte behövs.
- Slutligen berÀknas vÀrdena enligt formeln a + b * c. Resultatet omsluts sedan i ett löfte med hjÀlp av Promise.resolve och returneras av funktionen.
Dessa pauser kan likna generatorer i ES6, men det finns .
Att lösa problemet
LÄt oss nu titta pÄ lösningen till problemet som nÀmndes ovan.
I funktionen finishMyTask anvÀnds Await för att vÀnta pÄ resultat av operationer som queryDatabase, sendEmail, logTaskInFile och andra. Om man jÀmför denna lösning med den dÀr löften anvÀndes, blir likheterna uppenbara. Trots detta förenklar versionen med Async/Await betydligt alla syntaktiska komplexiteter. Det finns fÄ callbacks och kedjor som .then/.catch.
HÀr Àr en lösning med utskrift av siffror, det finns tvÄ alternativ.
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);
});
});
};HÀr Àr en lösning som anvÀnder async-funktioner.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}Felhantering
Ohanterade fel omsluts av ett rejected löfte. Trots detta kan man i async-funktioner anvÀnda try/catch för att göra en synkron felhantering.
async function canRejectOrReturn() {
// vÀnta en sekund
await new Promise(res => setTimeout(res, 1000));
// Avvisa med ~50% sannolikhet
if (Math.random() > 0.5) {
throw new Error('FörlÄt, numret Àr för stort.')
}
return 'perfekt nummer';
}canRejectOrReturn() Ă€r en asynkron funktion som antingen lyckas utföra sig (âperfekt nummerâ) eller misslyckas med ett fel (âFörlĂ„t, numret Ă€r för stortâ).
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'fel fÄngat';
}
}Eftersom canRejectOrReturn förvÀntas utföras ovan, kommer eget misslyckande att leda till att catch-blocket körs. Funktionen foo avslutas antingen med undefined (nÀr inget returneras i try-blocket), eller med fel fÄngat. SÄledes kommer denna funktion inte att fÄ ett misslyckande, eftersom try/catch tar hand om funktionen foo.
HÀr Àr ett annat exempel:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'fel fÄngat';
}
}Det Ă€r vĂ€rt att notera att canRejectOrReturn returneras frĂ„n foo-exemplet. Foo avslutas antingen med perfekt nummer eller returnerar ett fel Error (âFörlĂ„t, numret Ă€r för stortâ). Catch-blocket kommer aldrig att köras.
Problemet Àr att foo returnerar ett löfte frÄn canRejectOrReturn. DÀrför blir lösningen för funktionen foo en lösning för canRejectOrReturn. Koden i detta fall bestÄr av endast tvÄ rader:
try {
const promise = canRejectOrReturn();
return promise;
}Och sÄ hÀr blir det om man anvÀnder await och return tillsammans:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'fel fÄngat';
}
}I koden ovan kommer foo att avslutas bÄde med perfekt nummer och fel fÄngat. HÀr blir det inga misslyckanden. Men foo kommer att avslutas med canRejectOrReturn och inte med undefined. LÄt oss kontrollera detta genom att ta bort raden return await canRejectOrReturn():
try {
const value = await canRejectOrReturn();
return value;
}
// âŠVanliga misstag och fĂ€llor
I vissa fall kan anvÀndning av Async/Await leda till fel.
Glömt await
Detta hĂ€nder ganska ofta â nyckelordet await glöms bort före ett löfte:
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}I koden finns varken await eller return. DÀrför avslutas foo alltid med undefined utan nÄgon fördröjning pÄ 1 sekund. Men löftet kommer att exekveras. Om det ger ett fel eller avvisas, kommer UnhandledPromiseRejectionWarning att utlösas.
Async-funktioner i Äteranrop
Async-funktioner anvÀnds ofta i .map eller .filter som Äteranrop. Ett exempel Àr funktionen fetchPublicReposCount(username), som returnerar antalet offentliga repos pÄ GitHub. Anta att vi har tre anvÀndare vars statistik vi behöver. HÀr Àr koden för denna uppgift:
const url = 'https://api.github.com/users';
// Utility fn to fetch repo counts
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}Vi behöver konton för ArfatSalman, octocat, norvig. I det hÀr fallet gör vi:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});Det Àr vÀrt att notera await i Äteranropet .map. HÀr Àr counts en array av löften, och .map Àr en anonym Äteranrop för varje angiven anvÀndare.
Ăverdriven sekventiell anvĂ€ndning av await
Som exempel tar vi följande kod:
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;
}HÀr placeras antalet repos i variabeln count, och detta tal lÀggs till i arrayen counts. Problemet med koden Àr att sÄ lÀnge som data frÄn den första anvÀndaren inte har kommit, kommer alla efterföljande anvÀndare att vÀnta. SÄledes hanteras endast en anvÀndare Ät gÄngen.
Om det till exempel tar cirka 300 ms att behandla en anvÀndare, sÄ blir det för alla anvÀndare redan en sekund; den totala tiden beror linjÀrt pÄ antalet anvÀndare. Men eftersom antalet repos inte beror pÄ varandra kan processerna parallelliseras. För detta behövs arbete med .map och Promise.all:
asynkron funktion fetchAllCounts(anvÀndare) {
const löften = anvÀndare.map(async anvÀndarnamn => {
const antal = await fetchPublicReposCount(anvÀndarnamn);
return antal;
});
return Promise.all(löften);
}Promise.all tar emot en array av löften som returnerar ett löfte. Den avslutas först efter att alla löften i arrayen har slutförts eller vid första avvisningen. Det kan hĂ€nda att inte alla startar samtidigt â för att sĂ€kerstĂ€lla samtidig körning kan man anvĂ€nda p-map.
Slutsats
Asynkrona funktioner blir allt viktigare för utveckling. För adaptiv anvÀndning av asynkrona funktioner bör man övervÀga . En JavaScript-utvecklare bör ha god kunskap om detta.
Skillbox rekommenderar:
- Praktisk kurs .
- TillÀmplig onlinekurs .
- TvÄÄrig praktisk kurs .
KĂ€lla: habr.com
