Полный цикл в digital

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

Асинхронные итераторы объединяют возможности итераторов и операторов 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.

Заполните форму уже сегодня!
Для начала сотрудничества необходимо заполнить заявку или заказать обратный звонок. В ответ получите коммерческое предложение, которое будет содержать индивидуальную стратегию с учетом требований и поставленных задач
Работаем по будням с 9:00 до 18:00. Заявки, отправленные в выходные, обрабатываем в первый рабочий день до 12:00.
Спасибо, ваш запрос принят и будет обработан!
Эйч Маркетинг