Iteratoare Asincrone
Iteratoarele asincrone combină capacitățile iteratoarelor și ale operatorilor async și await. Iteratoarele asincrone sunt destinate în principal accesării surselor de date care folosesc API-uri asincrone. Acestea pot fi, de exemplu, date care se încarcă pe părți, cum ar fi datele de pe rețea, din sistemul de fișiere sau din baza de date.
Din articolul despre iteratoare, trebuie să ne amintim că un iterator oferă metoda next(), care returnează un obiect cu două proprietăți: { value, done }. Proprietatea value stochează o anumită valoare, pe care, de exemplu, o putem obține într-un ciclu for..of atunci când iterăm un obiect. Proprietatea done indică dacă iterarea obiectelor este finalizată.
Dacă această proprietate este false, înseamnă că iteratorul încă nu a finalizat iterarea obiectelor și sunt încă obiecte disponibile. Dacă proprietatea este true, atunci iterarea este finalizată, și în set nu mai sunt obiecte disponibile pentru iterare.
Un iterator asincron este similar cu un iterator sincron cu excepția faptului că metoda sa next() returnează un obiect Promise. Iar din acel promise, la rândul său, este returnat un obiect { value, done }.
Ciclul for-await-of
Pentru obținerea datelor cu ajutorul iteratoarelor asincrone se folosește ciclul for-await-of:
for await (variabila of iterabil) {
// acțiuni
}
În ciclul for-await-of, după operatorul of urmează un anumit set de date, care poate fi iterat element cu element. Acesta poate fi o sursă de date asincronă, dar poate fi și o sursă de date sincronă, cum ar fi array-uri sau, de exemplu, obiecte încorporate String, Map, Set etc.
Trebuie să notăm că această formă de ciclu poate fi utilizată doar în funcții definite cu operatorul async.
Să examinăm cel mai simplu exemplu, unde sursa de date este un array obișnuit:
const dataSource = ["Tom", "Sam", "Bob"];
async function readData(){
for await (const item of dataSource) {
console.log(item);
}
}
readData();
// Tom
// Sam
// Bob
Aici, în ciclu are loc iterarea array-ului dataSource. La executarea ciclului, pentru sursa de date (în acest caz, pentru array) se creează implicit un iterator asincron cu ajutorul metodei [Symbol.asyncIterator]().
Și la fiecare accesare a următorului element în această sursă de date, din iterator se returnează implicit un obiect Promise, din care obținem elementul curent al array-ului.
Crearea unui Iterator Asincron
În exemplul de mai sus, iteratorul asincron a fost creat implicit. Dar putem, de asemenea, să-l definim în mod explicit. De exemplu, să definim un iterator asincron care returnează elementele unui array:
const generatePerson = {
[Symbol.asyncIterator]() {
return {
index: 0,
people: ["Tom", "Sam", "Bob"],
next() {
if (this.index < this.people.length) {
return Promise.resolve({ value: this.people[this.index++], done: false });
}
return Promise.resolve({ done: true });
}
};
}
};
Aici este definit obiectul generatePerson, în care este implementat doar o singură metodă - [Symbol.asyncIterator](), care practic reprezintă iteratorul asincron. Implementarea iteratorului asincron (la fel ca și în cazul unui iterator sincron) permite să facem obiectul generatePerson iterabil.
Puncte principale ale iteratorului asincron:
- Iteratorul asincron este implementat prin metoda [Symbol.asyncIterator](), care returnează un obiect
- Obiectul returnat de iterator are metoda next(), care returnează un obiect Promise
- Obiectul Promise, la rândul său, returnează un obiect cu două proprietăți { value, done }. Proprietatea value stochează o anumită valoare. Proprietatea done indică dacă iterarea este finalizată și, corespunzător, dacă în set mai sunt obiecte disponibile pentru iterare. Dacă proprietatea done este true (iterarea este finalizată și nu mai sunt obiecte disponibile), atunci nu este necesar să specificăm proprietatea value
În acest caz, iteratorul realizează o sarcină simplă - returnează următorul utilizator. Pentru stocarea utilizatorilor în obiectul iteratorului este definit array-ul people, iar pentru stocarea indexului elementului curent al array-ului este definită variabila index.
index: 0,
people: ["Tom", "Sam", "Bob"],
În metoda next() returnăm un obiect Promise. Dacă indexul curent este mai mic decât lungimea array-ului (adică în array mai sunt elemente disponibile pentru iterare), atunci returnăm un Promise în care returnăm elementul array-ului la indexul curent:
return Promise.resolve({ value: this.people[this.index++], done: false });
Dacă toate elementele array-ului au fost deja obținute, atunci returnăm un Promise cu obiectul { done: true }:
return Promise.resolve({ done: true });
Unde valoarea done: true va indica codului extern că toate valorile iteratorului au fost deja obținute.
Acum să vedem cum putem obține date din iterator:
La fel ca și cu un iterator obișnuit, ne putem adresa direct la iteratorul asincron în sine:
generatePerson[Symbol.asyncIterator](); // obținem iteratorul asincron
Și să apelăm în mod explicit metoda sa next():
generatePerson[Symbol.asyncIterator]().next(); // Promise
Această metodă returnează un Promise, al cărui metodă then() poate fi apelată pentru a procesa valoarea sa:
generatePerson[Symbol.asyncIterator]()
.next()
.then((data)=>console.log(data)); // {value: "Tom", done: false}
Obiectul obținut din promise reprezintă un obiect {value, done}, iar prin proprietatea value se poate obține valoarea efectivă:
generatePerson[Symbol.asyncIterator]()
.next()
.then((data)=>console.log(data.value)); // Tom
Deoarece metoda next() returnează un Promise, putem folosi operatorul await pentru a obține valorile:
async function printPeople(){
const peopleIterator = generatePerson[Symbol.asyncIterator]();
while(!(personData = await peopleIterator.next()).done){
console.log(personData.value);
}
}
printPeople();
Aici, într-o funcție asincronă, într-un ciclu while obținem în mod secvențial, folosind operatorul await, obiecte Promise de la iterator, din care extragem date, până când ajungem la sfârșitul datelor iteratorului.
Totuși, pentru a itera un obiect cu iterator asincron este mult mai simplu să folosim ciclul for-await-of discutat mai sus:
const generatePerson = {
[Symbol.asyncIterator]() {
return {
index: 0,
people: ["Tom", "Sam", "Bob"],
next() {
if (this.index < this.people.length) {
return Promise.resolve({ value: this.people[this.index++], done: false });
}
return Promise.resolve({ done: true });
}
};
}
};
async function printPeople(){
for await (const person of generatePerson) {
console.log(person);
}
}
printPeople();
Deoarece obiectul generatePerson implementează metoda [Symbol.asyncIterator](), îl putem itera folosind ciclul for-await-of. Astfel, la fiecare accesare în ciclu, metoda next() va returna un promise cu următorul element din array-ul people. Și, în final, vom obține următorul output în consolă:
Tom
Sam
Bob
Trebuie să notăm că NU putem folosi un ciclu for-of obișnuit pentru a itera un obiect cu iterator asincron.
Un alt exemplu simplu - obținerea numerelor:
const generateNumber = {
[Symbol.asyncIterator]() {
return {
current: 0,
end: 10,
next() {
if (this.current <= this.end) {
return Promise.resolve({ value: this.current++, done: false });
}
return Promise.resolve({ done: true });
}
};
}
};
async function printNumbers(){
for await (const n of generateNumber) {
console.log(n);
}
}
printNumbers();
Aici, iteratorul asincron al obiectului generateNumber returnează numerele de la 0 la 10.