Асинхронные итераторы

Последнее обновление: 04.09.2021

Асинхронные итераторы объединяют возможности итераторов и операторов async и await. Асинхронные итераторы прежде всего предназначены для обращения к источникам данных данных, которые используют асинхронный API. Это могут быть какие-нибудь данные, которые загружаются по части, например, по сети, из файловой системы или из базы данных.

Из статьи про итераторы мы должны помнить, что интератор предоставляет метод next(), который возвращает объект с двумя свойствами: { value, done }. Свойство value хранит некоторое значение, которое, например, можно получить в цикле for..of при переборе объекта. А свойство done указывает, завершен ли перебор объектов. Если это свойство равно false, значит, итератор еще не завершил перебор объектов, и есть еще доступные объекты. Если свойство равно true, то перебор закончен, и в наборе больше нет доступных для перебора объектов.

Асинхронный итератор похож на обычный синхронный за тем исключением, что его метод 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 равно true (перебор закончен, и доступных для перебора объектов больше нет), то нет смысла указывать свойство 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()
	.then((data)=>console.log(data));	// {value: "Tom", done: false}	

Полученный из промиса объект представляет объект {value, done}, у которого через свойство value можно получить собственно значение:

generatePerson[Symbol.asyncIterator]()
	.next()
	.then((data)=>console.log(data.value));	// Tom

Поскольку метод 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.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850