Асинхронные итераторы
Асинхронные итераторы объединяют возможности итераторов и операторов async
и await
. Асинхронные итераторы прежде всего предназначены для обращения к источникам данных данных, которые используют асинхронный API
. Это могут быть какие-нибудь данные, которые загружаются по части сети из файловой системы или из базы данных.
Из статьи про итераторы мы должны помнить, что интератор предоставляет метод next()
, который возвращает объект с двумя свойствами: { value, done }
. Свойство value
хранит некоторое значение, которое можно получить в цикле for..of
при переборе объекта. А свойство done
указывает, есть ли в наборе еще объекты доступные для перебора. Если есть, то значение true
, если нет false
.
Асинхронный итератор похож на обычный синхронный за тем исключением, что его метод next()
возвращает объект Promise
. А из промиса в свою очередь, возвращается объект { value, done }
.
Цикл for-await-of
Для получения данных с помощью асинхронных итераторов применяется цикл for-await-of
:
for await (variable of iterable) {
// действия
}
В цикле for-await-of
после оператора of
идет некоторый набор данных, который можно перебрать по элементам. Это может асинхронный источник данных, но также может быть и синхронный источник данных, как массивы или например, встроенные объекты String
, Map
, Set
и т.д.
Стоит отметить, что данная форма цикла может использоваться только в функциях, определенных с оператором async
.
Рассмотрим простейший пример, где в качестве источника данных выступает обычный массив:
const dataSource = ["Tom", "Sam", "Bob"];
async function readData() {
for await (const item of dataSource) {
console.log(item);
}
}
readData();
// Tom
// Sam
// Bob
Здесь в цикле происходит перебор массива dataSource
. При выполнении цикла для источника данных, в данном случае для массива с помощью метода [Symbol.asyncIterator]()
неявно создается асинхронный итератор. И при каждом обращении к очередному элементу в этом источнике данных неявно из итератора возвращается объект Promise
, из которого и получаем текущий элемент массива.
Создание асинхронного итератора
В примере выше асинхронный итератор создавался неявно. Но мы также можем его определить явно. Например, определим асинхронный итератор, который возвращает элементы массива:
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 });
},
};
},
};
Итак, здесь определен объект generatePerson
, в котором реализован только один метод [Symbol.asyncIterator]()
, который по сути и представляет асинхронный итератор. Реализация асинхронного итератора, как и в случае с синхронным итератором позволяет сделать объект generatePerson
перебираемым. Основные моменты асинхронного итератора:
- Асинхронный итератор реализуется методом
[Symbol.asyncIterator]()
, который возвращает объект - Возвращаемый объект итератора имеет метод
next()
, который возвращает объектPromise
- Объект
Promise
, в свою очередь, возвращает объект с двумя свойстами{ value, done }
. Свойствоvalue
собственно хранит некоторое значение. А свойствоdone
указывает, есть ли в наборе еще объекты, доступные для перебора. Если свойствоdone
равноfalse
, то нет смысла указывать свойствоvalue
В данном случае итератор реализует простую задачу - возвращает очереднего пользователя. Для хранения пользователей в объекте итератора определен массив people
, а для хранения индекса текущего элемента массива определена переменная index
.
index: 0,
people: ["Tom", "Sam", "Bob"],
В методе next()
возвращаем объект Promise
. Если текущий индекс меньше длины массивы (то есть в массиве еще имеются для перебора элементы), тогда возвращаем Promise
, в котором возвращаем элемент массива по текущему индексу:
return Promise.resolve({ value: this.people[this.index++], done: false });
Если все элементы массива уже получены, то возвращаем Promise
с объектом { done: true }
:
return Promise.resolve({ done: true });
Где значение done: true
будет указывать внешнему коду, что все значения итератора уже получены.
Теперь посмотрим, как мы можем получить из итератора данные.
Как и с обычным итератором, мы можем обратиться к самому асинхронному итератору:
generatePerson[Symbol.asyncIterator](); // получаем асинхронный итератор
И вызвать явным образом его метод next()
:
generatePerson[Symbol.asyncIterator]().next(); // Promise
Этот метод возвращает Promise
, у котоого можно вызвать метод then()
и обработать его значение:
generatePerson[Symbol.asyncIterator]()
.next()
// {value: "Tom", done: false}
.then((data) => console.log(data));
Полученный из промиса объект представляет объект { value, done }
, у которого через свойство value
можно получить собственно значение:
generatePerson[Symbol.asyncIterator]()
.next()
// Tom
.then((data) => console.log(data.value));
Поскольку метод next()
возвращает Promise
, то мы можем использовать оператор await
для получения значений:
async function printPeople() {
const peopleIterator = generatePerson[Symbol.asyncIterator]();
while (!(personData = await peopleIterator.next()).done) {
console.log(personData.value);
}
}
printPeople();
Здесь в асинхронной функции цикле while
с помощью оператора await
последовательно получаем из итератора один за другим объекты Promise
, из которых извлекаем данные, пока не достигнем конца данных итератора.
Однако для перебора объекта асинхронного итератора гораздо проще использовать выше рассмотренный цикл for-await-of
:
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();
Поскольку объект generatePerson реализует метод [Symbol.asyncIterator]()
, то мы его можем перебрать с омощью цикла for-await-of
. Соответственно при каждом обращении в цикле метод next()
будет возращать промис с очередным элементом из массива people
. И в итоге мы получим следующий консольный вывод:
Tom
Sam
Bob
Стоит отметить, что мы не можем использовать для перебора объекта с асинхронным итератором обычный цикл for-of
.
Еще один простейший пример - получение чисел:
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();
Здесь асинхронный итератор объекта generateNumber возвращает числа от 0
до 10
.