Итераторы и генераторы
До сих пор функция возвращала весь массив результата сразу. Итератор предоставляет значения последовательно по запросу, а генератор позволяет описать такую последовательность обычным циклом с остановками. Этот механизм полезно понять отдельно от сети: ленивое получение значения не означает автоматического HTTP-запроса.
Снимок advanced/lesson-32 делит четыре курса на части по две записи. Данные уже находятся в памяти, поэтому не повторяем постраничный API урока 24. Результаты представлены как ожидаемые: программа и её вывод при подготовке не проверялись запуском.
Протокол следующего значения
Итератор имеет операцию next, возвращающую объект с value и done. Пока последовательность продолжается, value содержит очередной результат, а done показывает завершение. Это не обычный массив с индексами: состояние итератора изменяется после каждого запроса.
Генератор создаётся объявлением function*. Внутри yield отдаёт значение и приостанавливает выполнение до следующего продвижения. Наша полная функция:
export function* chunks(rows, size) {
if (!Number.isInteger(size) || size < 1) {
throw new TypeError('Размер должен быть положительным целым');
}
for (let offset = 0; offset < rows.length; offset += size) {
yield rows.slice(offset, offset + size);
}
}
Каждая порция является новым массивом, полученным через slice. Исходный порядок курсов сохраняется, а объекты внутри остаются прежними ссылками. Функция не меняет данные и не вычисляет новый список карточек в документе. Механизм итераторов и генераторов описан в MDN.
Вызов chunks(courses, 2) создаёт объект генератора, но тело ещё не проходит первый цикл. Даже проверка размера внутри тела произойдёт при продвижении. Поэтому ожидаемый отказ при неправильном размере следует наблюдать на next или обходе, а не обещать немедленно в момент создания.
Два шага и завершение
После импорта данных используем отдельный экземпляр:
const iterator = chunks(courses, 2);
console.log(iterator.next().value.map(course => course.id));
console.log(iterator.next().value.map(course => course.id));
console.log(iterator.next());
Первая порция содержит javascript и html-css, вторая — performance и markdown. Третье продвижение ожидаемо сообщает done: true и значение undefined, поскольку явного возвращаемого результата завершения нет. Ещё одно продвижение не начнёт последовательность заново.
Вызов next не выбирает произвольную страницу по номеру. Он продолжает текущий проход. Если интерфейсу нужен переход назад или непосредственное открытие второй страницы по адресу, одного такого генератора недостаточно. В уроке 24 для этого существовал отдельный контракт запроса и конверта.
Чтобы начать заново, создайте новый экземпляр через новый вызов chunks. Исходные данные остаются доступны, поэтому последовательность воспроизводима. Передача одного уже частично использованного итератора нескольким потребителям может привести к неожиданному распределению порций: каждый продвигает общее состояние.
Обход через for…of
Генератор поддерживает протокол итерируемого значения, поэтому его можно передать знакомому циклу:
for (const page of chunks(courses, 2)) {
console.log(page.length);
}
Ожидаются два сообщения со значением два. Цикл сам вызывает next, читает done и получает значения. Это тот же вид for…of, который раньше обходил массив, но теперь источник предоставляет порции по собственному правилу.
Spread или Array.from тоже могут потребить последовательность целиком. После такого преобразования исчезает преимущество получения только части: все порции уже созданы. Поэтому не называйте генератор ленивым решением, если следующий участок немедленно материализует весь результат без причины.
Для четырёх карточек экономия не является существенным измеренным преимуществом. Здесь изучаем протокол и обязанности. На большом источнике выбор между полным результатом и постепенным потреблением требует анализа памяти, времени и доступа к исходным данным, а не рекламной фразы о скорости.
Время жизни прохода
При остановке for…of через break генератор может завершиться досрочно. В более сложной функции finally позволяет освободить связанный ресурс. Наш пример не открывает файлов и соединений, поэтому не нуждается в искусственной операции очистки ради демонстрации.
Если данные массива меняются во время прохода, будущие порции могут увидеть изменённую структуру. Ленивое исполнение не является снимком автоматически. Наша договорённость сохраняет исходные записи и порядок; если приложению нужна стабильная версия, сначала создаётся соответствующая копия или фиксируется версия источника.
Асинхронный генератор имеет другой способ продвижения и используется с for await…of. Не добавляйте async к этой функции только потому, что в соседних уроках был fetch. Здесь значения уже доступны синхронно. Механизм должен соответствовать источнику и цели, а не общей моде на асинхронность.
Сопоставление с массивом
Массив можно обходить повторно с нового начала, создавая новый проход. Конкретный объект генератора хранит уже пройденное состояние. Это различие важно при передаче в функцию отображения: она может предполагать повторное использование входа и случайно получить только остаток последовательности.
Удобно передавать фабрику генераторов, когда потребителю нужен независимый проход. Тогда каждый вызов создаст новый итератор. Для небольшого каталога массив остаётся основным форматом данных, а генератор выражает отдельную операцию разбиения. Мы не переписываем все функции модели под иной контракт без необходимости.
Ручной опыт должен сравнить первый и второй вызовы next, завершение, повторное продвижение и новый экземпляр. Затем попробуйте размер три: ожидаются порции из трёх и одной записи. Для размера ноль продвижение должно вызвать наш отказ. После упражнения верните размер два, чтобы дальнейшие наблюдения сохраняли знакомые данные.
Кто владеет продвижением последовательности
Передача одного итератора двум функциям означает передачу общего изменяемого состояния прохода. Если первая функция получила первую порцию, вторая начнёт со второй, даже если ожидала полный набор. Это похоже на общий указатель чтения, а не на два независимых взгляда на массив. Для независимости передавайте исходный массив или фабрику, создающую отдельный генератор каждому потребителю.
Объект результата next тоже нужно читать по договору. После завершения нельзя безусловно вызвать map у value: там ожидается undefined. Проверка done предшествует использованию очередной порции. В кратком снимке первые два вызова известны из размера данных, а третий выводит весь объект. Для произвольного источника такой подсчёт заранее неизвестен, поэтому обход через for…of обычно яснее.
Размер три помогает увидеть остаток: первая порция содержит три записи, вторая одну, затем проход завершён. Размер больше массива даст одну порцию со всеми четырьмя. Пустой массив не отдаст ни одной порции, но проверка допустимого размера всё равно выполнится при первом продвижении. Эти случаи показывают отдельно правило разбиения и отсутствие данных, не смешивая пустоту с неверным аргументом.
Поскольку функция использует slice, полученный массив порции можно изменить без изменения длины исходного массива. Однако объект курса внутри остаётся той же ссылкой. Если изменить его название, изменение увидит и источник. Это знакомая граница поверхностного копирования, которую генератор не отменяет. В нашем опыте порции читаются, а исходные записи не редактируются во время прохода.
Не выводите из остановки yield способность выполнять работу в другом потоке. Следующее продвижение продолжает синхронную функцию в текущем исполнении. Если вычисление между остановками дорогое, оно всё ещё может задержать интерфейс. Другие механизмы, включая Worker далее в серии, имеют отдельную обязанность. Генератор здесь даёт управляемую последовательность порций и ясное время получения следующего значения.
Теперь последовательность имеет явный протокол продвижения. Следующий урок рассмотрит вызов самой функции: значение this зависит от способа вызова, поэтому метод объекта и переданный отдельно callback могут вести себя по-разному.