Raksta autors izskaidro ar piemēriem Async/Await JavaScript. Kopumā, Async/Await ir ērta pieeja asinsynchronous koda rakstīšanai. Pirms šīs iespējas šādu kodu rakstīja, izmantojot atgriezeniskās saites un solījumus. Oriģinālā raksta autors atklāj Async/Await priekšrocības, analizējot dažādus piemērus.
Atgādinām: visiem «Habr» lasītājiem — atlaide 10 000 rubļu reģistrējoties jebkurā Skillbox kursā ar promo kodu «Habr».
Skillbox iesaka: Izglītības tiešsaistes kurss .
Atgriezeniskā saite
Atgriezeniskā saite ir funkcija, kuras izsaukšana tiek atlikta uz nenoteiktu laiku. Agrāk atgriezeniskās saites tika izmantotas to koda daļu vietās, kur rezultāts nevarēja tikt iegūts uzreiz.
Šeit ir piemērs asynhrona faila lasīšanai Node.js:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
}); Problēmas rodas brīdī, kad nepieciešams veikt vairākas asinsynchronous operācijas uzreiz. Iedomāsimies šādu scenāriju: tiek veikts pieprasījums uz datu bāzi par lietotāju Arfat, ir jāizlasa viņa lauks profile_img_url un jāsagaida attēls no servera someserver.com.
Pēc attēla augšupielādes mēs to konvertējam citā formātā, piemēram, no PNG uz JPEG. Ja konversija ir veiksmīga, lietotājam tiek nosūtīta e-pasta vēstule. Pēc tam informācija par notikumu tiek ierakstīta failā transformations.log, norādot datumu.
Ir vērts pievērst uzmanību atgriezenisko zvanu pārslogotībai un lielajam skaitam }) gala daļā kodā. To sauc par Callback Hell vai Pyramid of Doom.
Šāda paņēmiņa trūkumi ir acīmredzami:
- Šo kodu ir grūti lasīt.
- Tajā arī ir grūti apstrādāt kļūdas, kas bieži noved pie kodeļa kvalitātes pasliktināšanās.
Lai risinātu šo problēmu, JavaScript tika pievienoti solījumi. Tie ļauj nomainīt dziļo atgriezeniskās saites iegremdēšanu uz vārdu .then.
Pozitīva solījumu puse ir tāda, ka ar tiem kodu ir daudz vieglāk lasīt, turklāt no augšas uz leju, nevis no kreisās uz labo. Tomēr solījumiem arī ir savas problēmas:
- Jāpieņem liels skaits .then.
- Vietā try/catch tiek izmantots .catch, lai apstrādātu visas kļūdas.
- Darbs ar vairākiem solījumiem vienā ciklā ne vienmēr ir ērts, dažos gadījumos tie sarežģī kodu.
Šeit ir uzdevums, kas parādīs pēdējā punkta nozīmi.
Pieņemsim, ka ir for cikls, kas izvada skaitļus no 0 līdz 10 ar nejaušu intervālu (0–n sekundes). Izmantojot solījumus (promises), šis cikls jāmaina tā, lai skaitļi tiktu izvadīti secībā no 0 līdz 10. Tātad, ja nulles izvadīšana aizņem 6 sekundes, bet vieninieka — 2 sekundes, vispirms jāizvada nulle, un tikai tad var sākties vieninieka izvadīšana.
Un protams, šīs problēmas risināšanai mēs neizmantojam Async/Await vai .sort. Risinājuma piemērs būs beigās.
Async funkcijas
Async funkciju pievienošana ES2017 (ES8) atviegloja darbu ar solījumiem. Jāatzīmē, ka async funkcijas darbojas "virs" solījumiem. Šīs funkcijas nav kvalitatīvi citi koncepti. Async funkcijas tika domātas kā alternatīva kodam, kas izmanto solījumus.
Async/Await sniedz iespēju organizēt darbu ar asinhrono kodu sinhronā stilā.
Tādējādi solījumu zināšanas atvieglo Async/Await principu izpratni.
Sintakse
Parastā situācijā tas sastāv no diviem atslēgvārdiem: async un await. Pirmais atslēgvārds pārvērš funkciju par asinhronu. Šādās funkcijās ir atļauts izmantot await. Jebkurā citā gadījumā šīs funkcijas izmantošana izraisīs kļūdu.
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
Async tiek ievietots funkcijas deklarēšanas sākumā, bet, izmantojot bultiņu funkciju, starp zīmi "=" un iekavām.
Šīs funkcijas var ievietot objektā kā metodes vai izmantot klases deklarācijā.
// 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! Jāatceras, ka klases konstruktori un getteri/setteri nevar būt asinhroni.
Semantika un izpildes noteikumi
Async funkcijas principā ir līdzīgas standarta JS funkcijām, taču ir arī izņēmumi.
Tātad, async funkcijas vienmēr atgriež solījumus:
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloĪpaši, fn atgriež virkni hello. Tā kā tā ir asinhrona funkcija, šīs virknes vērtība tiek iesaiņota solījumā ar konstruktora palīdzību.
Šeit ir alternatīva konstrukcija bez Async:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// helloŠajā gadījumā solījuma atgriešana tiek veikta "roku darbs". Asinhrona funkcija vienmēr tiek iesaiņota jaunā solījumā.
Ja gadījumā, ja atgrieztais vērtība ir primitīvs, async-funkcija veic atgriešanu, iesaiņojot to solījumā. Ja atgrieztais vērtība ir solījuma objekts, tā risinājums tiek atgriezts jaunā solījumā.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Bet kas notiks, ja iekšpusē asinkronās funkcijas notiks kļūda?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);Ja tā netiks apstrādāta, foo() atgriezīs solījumu ar atteikumu. Šajā situācijā vietā Promise.resolve tiks atgriezts Promise.reject, kurā ir kļūda.
Async-funkcijas vienmēr atgriež solījumu, neatkarīgi no tā, kas tiek atgriezts.
Asinkronās funkcijas apstājas pie katra await.
Await ietekmē izteiksmes. Tādējādi, ja izteiksme ir solījums, async-funkcija apstājas līdz solījuma izpildei. Gadījumā, ja izteiksme nav solījums, tā tiek konvertēta par solījumu caur Promise.resolve un pēc tam tiek pabeigta.
// 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);Te ir apraksts par to, kā darbojas fn-funkcija.
- Pēc tās izsaukuma pirmā rinda tiek konvertēta no const a = await 9; uz const a = await Promise.resolve(9);.
- Pēc Await izmantošanas funkcijas izpilde tiek apturēta, līdz a saņem savu vērtību (šajā situācijā tas ir 9).
- delayAndGetRandom(1000) aptur fn-funkcijas izpildi, kamēr tā nepabeidz (pēc 1 sekundes). Tas faktiski ir fn-funkcijas apstāšanās uz 1 sekundi.
- delayAndGetRandom(1000) caur resolve atgriež nejaušu vērtību, kas tad tiek piešķirta mainīgajam b.
- Bet gadījums ar mainīgo c ir līdzīgs gadījumam ar mainīgo a. Pēc tam viss apstājas uz sekundi, bet tagad delayAndGetRandom(1000) nekas netiek atgriezts, jo tas nav nepieciešams.
- Rezultātā vērtības tiek aprēķinātas ar formulu a + b * c. Rezultāts tiek iesaiņots solījumā, izmantojot Promise.resolve, un tiek atgriezts no funkcijas.
Šie pārrāvumi var atgādināt ģeneratorus ES6, bet tam ir .
Risinām uzdevumu
Tagad aplūkosim iepriekš minētā uzdevuma risinājumu.
Function finishMyTask uses Await to wait for the results of operations such as queryDatabase, sendEmail, logTaskInFile, and others. When comparing this solution to one using promises, the similarities become clear. However, the version with Async/Await significantly simplifies all syntactic complexities. In this case, there aren't many callbacks and chains like .then/.catch.
Here’s a solution to output numbers, with two variants available.
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);
});
});
};And here’s a solution using async functions.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}Error handling
Unhandled errors are wrapped in a rejected promise. However, in async functions, the try/catch construct can be used for synchronous error handling.
async function canRejectOrReturn() {
// wait one second
await new Promise(res => setTimeout(res, 1000));
// Reject with ~50% probability
if (Math.random() > 0.5) {
throw new Error('Sorry, number too big.')
}
return 'perfect number';
}canRejectOrReturn() is an asynchronous function that either successfully completes (“perfect number”) or fails with an error (“Sorry, number too big”).
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}Since canRejectOrReturn is awaited in the above example, an unsuccessful completion will trigger the catch block. As a result, the foo function will finish either with undefined (when nothing is returned in the try block) or with error caught. Thus, this function will not have a failure since the try/catch will handle foo itself.
Here’s another example:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}It’s important to note that in the example from foo, canRejectOrReturn is returned. Foo in this case completes either with perfect number or an Error (“Sorry, number too big”). The catch block will never execute.
The problem is that foo returns a promise from canRejectOrReturn. Therefore, the solution of the foo function becomes the solution for canRejectOrReturn. In this case, the code will consist of just two lines:
try {
const promise = canRejectOrReturn();
return promise;
}And here’s what happens if you use await and return together:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}Iepriekš minētajā kodā foo veiksmīgi pabeigsies gan ar perfect number, gan ar error caught. Šeit atteikumu nebūs. Tomēr foo pabeigsies ar canRejectOrReturn, nevis ar undefined. Pārliecināsimies par to, izņemot rindu return await canRejectOrReturn():
try {
const value = await canRejectOrReturn();
return value;
}
// …Izplatītas kļūdas un slēptās bedres
Dažos gadījumos Async/Await izmantošana var radīt kļūdas.
Aizmirsts await
Tas notiek diezgan bieži — pirms solījuma tiek aizmirsts atslēgvārds await:
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}Kods, kā redzams, nesatur ne await, ne return. Tāpēc foo vienmēr pabeidzas ar undefined bez 1 sekundes kavēšanās. Tomēr solījums tiks izpildīts. Ja tas tomēr izsniegs kļūdu vai tiks atteikts, šajā gadījumā tiks izsaukts UnhandledPromiseRejectionWarning.
Async funkcijas atgriezienu saucienos
Async funkcijas diezgan bieži izmanto .map vai .filter kā atgriezienu saucienos. Piemēram, var minēt funkciju fetchPublicReposCount(username), kas atgriež atvērtā GitHub repozitoriju skaitu. Pieņemam, ka mums ir trīs lietotāji, kuru rādītājus mēs vēlamies iegūt. Šeit ir kods šai uzdevumam:
const url = 'https://api.github.com/users';
// Palīgfunkcija, lai iegūtu repozitoriju skaitu
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}Mums nepieciešami konti ArfatSalman, octocat, norvig. Šajā gadījumā veicam:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});Jāņem vērā Await atgriezienu saucienā .map. Šeit counts ir solījumu masīvs, un .map ir anonīms atgriezienu sauciens katram norādītajam lietotājam.
Pārmērīga sekvencēla await izmantošana
Piemēram, apskatīsim šādu kodu:
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;
}Šeit mainīgajā count tiek ievietots repozitoriju skaits, pēc tam šis skaitlis tiek pievienots masīvam counts. Koda problēma ir tāda, ka, kamēr no servera nenāk datu pirmā lietotāja, visi pārējie lietotāji gaida. Tādējādi vienlaikus tiek apstrādāts tikai viens lietotājs.
Ja, piemēram, ja viena lietotāja apstrādei nepieciešamas apmēram 300 ms, tad visiem lietotājiem tas jau ir sekunde, izmaksātā laika atkarība lineāri mainās, atkarībā no lietotāju skaita. Bet, ņemot vērā, ka repo skaita iegūšana nav atkarīga viena no otras, procesus var paralelizēt. Tam nepieciešama .map un Promise.all izmantošana:
async function fetchAllCounts(users) {
const promises = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
return Promise.all(promises);
}Promise.all saņem masīvu ar solījumiem un atgriež solījumu. Tas beidzas pēc visu solījumu pabeigšanas masīvā vai pie pirmā atteikuma. Var gadīties, ka tie visi nenonāk uzsākšanās laikā, — lai nodrošinātu vienlaicīgu palaišanu, var izmantot p-map.
Secinājums
Async funkcijas kļūst arvien svarīgākas izstrādē. Bet adaptīvai async funkciju izmantošanai ir vērts izmantot . JavaScript izstrādātājam jābūt labi pārzinošam sulū.
Skillbox iesaka:
- Praktiskais kurss .
- Praktiskais tiešsaistes kurss .
- Divu gadu praktiskais kurss .
Avots: habr.com
