Не бойтесь генераторов JavaScript

Ответьте честно на вопрос: часто ли вы сталкиваетесь с кодом, в котором используются генераторы? Я ежедневно просматриваю код различных разработчиков, но редко встречаю генераторы.

Почему так происходит? Разработчики не знают, как их использовать? Или не ценят их преимуществ?

Генераторы были представлены в ECMAScript 2015, основном стандарте языка JavaScript, известного своей гибкостью и широким спектром возможностей. Это мощные средства для управления асинхронным программированием, создания итерируемых объектов и выдачи нескольких значений. В сегодняшнем гайде мы рассмотрим механизм работы генераторов, их применение и способы использования их потенциала.

Как работают генераторы?

Генераторы отличаются от традиционных функций. Они могут начинать и останавливать свое выполнение несколько раз. Это позволяет им выдавать множество значений и продолжать их выполнение в дальнейшем, что делает их идеальными для управления асинхронными операциями, построения итераторов и работы с бесконечными потоками данных.

Генератор отличается синтаксисом function*. Рассмотрим его на базовом примере:

function* generateSequence() {
yield 1;
yield 2;
yield 3;
}

Здесь yield возвращает значение и останавливает выполнение генератора. При каждом вызове генератор выдает последующее значение.

Взаимодействие с объектами генератора

Вызов функции-генератора не приводит к непосредственному запуску ее тела. Вместо этого создается объект Generator, позволяющий управлять ее выполнением. Поскольку этот объект является итерируемым, его можно использовать в циклах for...of и других подобных операциях.

Разберем объект Generator.

  • next(). Этот метод возобновляет работу генератора, возвращает следующее выдаваемое значение и показывает, завершилась ли работа генератора, с помощью свойства done. Используем для демонстрации предыдущий пример generateSequence:
console.log(generator.next()); // { value: 1, done: false }
  • return(). Этот метод преждевременно завершает работу генератора, как если бы вы выполнили команду return.
console.log(numbers.return(100)); // { value: 100, done: true }
  • throw(). Этот метод позволяет вставить ошибку, облегчая обработку ошибок непосредственно внутри генератора.
function* generateTasks() {
try {
yield "Start task";
yield "Continue task";
yield "Almost done with task";
} catch (error) {
console.log('A problem occurred:', error.message);
}
}

const tasks = generateTasks();

console.log(tasks.next().value); // Вывод: "Start task"
console.log(tasks.next().value); // Вывод: "Continue task"
tasks.throw(new Error('Oops! Something went wrong.'));
// Вывод: "A problem occurred: Oops! Something went wrong."
console.log(tasks.next()); // Вывод: { value: undefined, done: true }

В приведенном примере после инициирования нескольких задач с помощью метода next() вводим ошибку, используя метод throw(). Благодаря блоку try-catch генератор перехватывает эту ошибку, регистрирует сообщение о ней и изящно справляется со сценарием ошибки.

Использование генераторов для работы с бесконечными потоками данных

Генераторы прекрасно справляются с управлением бесконечными потоками данных. Вы можете создать потенциально бесконечную структуру данных, выдавая значения только по запросу. Вспомните ситуации, подобные бесконечной прокрутке в веб-приложениях.

function* infiniteNumbers() {
let index = 0;
while (true) {
yield index++;
}
}

Признаюсь, в этом и заключается для меня магия генераторов, хотя while(true) на первый взгляд выглядит довольно пугающе.

Синхронная и асинхронная итерация с помощью генераторов

В сочетании с промисами, генераторы могут эмулировать паттерн async/await, предлагая более аккуратный и интуитивно понятный метод создания асинхронного кода. Для примера выполним получение данных с помощью генератора:

function* fetchData() {
const users = yield fetch('https://api.example.com/users');
console.log('Users:', users);
// ...
}

Расширенное использование генераторов

Если async/await является оптимальным решением для простых асинхронных задач, то генераторы, обладая расширенными возможностями, обеспечивают универсальность.

  • Композиция генераторов: позволяет плавно объединять несколько генераторов, создавая сложные последовательности значений.
function* generateSequence() {
yield* generateNumbers();
yield* generateCharacters('A', 'Z');
}
  • Бесконечные генераторы: генераторы могут создавать бесконечные последовательности значений, что идеально подходит для непрерывных потоков данных или бесконечных алгоритмов. Помните приведенный выше пример с while(true)?

Реальная задача: бесконечная прокрутка

Может показаться, что концептуальное реальное применение JavaScript-генераторов затруднительно. Однако они легко интегрируются с асинхронным кодом и, помимо прочего, поддерживают бесконечные итерации. Проверим это на примере.

Дисклеймер: приведенный ниже код является чисто иллюстративным. В готовом к производству коде необходимо будет обработать множество граничных случаев.

Предлагаю создать ленту социальной сети, поддерживающую бесконечную прокрутку. Другими словами, по мере того как пользователь прокручивает список до конца, из него извлекаются и добавляются в ленту дополнительные посты.

Второй дисклеймер: генераторы, хотя и предлагают один из подходов, не являются исключительными в экосистеме JavaScript. Существуют и альтернативные методы достижения аналогичных результатов. Тем не менее в целях обучения разработаем механизм, который будет непрерывно получать посты по мере прокрутки ленты пользователем.

Вначале создадим базовую HTML/CSS-структуру для размещения данных, чтобы поэкспериментировать с ней:

// CSS-код 
.post {
height: 300px;
}

// HTML-код
<div id="postsContainer">

</div>

Теперь рассмотрим скрипт, предназначенный для запроса “10 постов”. По мере того как пользователь прокручивает страницу и приближается к ее концу, включается генератор для получения последующих 10 постов:

// Это просто замена обычного `fetch`
// Создает и возвращает фрагмент из 10 постов
async function simulatedFetch(currentPage) {
const posts = Array.from({ length: 10 }, (_, i) => ({ content: `Post - ${currentPage}${i}` }));
return Promise.resolve(posts)
}

async function* paginatedFetcher(apiUrl, itemsPerPage) {
let currentPage = 0;

while (true) {
// Кооментирование того, что было бы реальным кейсом
// const response = await fetch(`${apiUrl}?page=${currentPage}&limit=${itemsPerPage}`);
const response = await simulatedFetch(currentPage)

// const posts = await response.json();
const posts = response;

if (posts.length === 0) {
return; // конец данных
}

yield posts;
currentPage++;
}
}

// Случай использования с бесконечной прокруткой
// API носит иллюстративный характер и в данном примере не используется
const getPosts = paginatedFetcher('https://api.example.com/posts', 10);

// Функция для отображения постов в DOM
function displayPosts(posts) {
const container = document.getElementById('postsContainer');
posts.forEach(post => {
const postElement = document.createElement('div');
postElement.className = 'post';
postElement.innerText = post.content;
container.appendChild(postElement);
});
}

// Логика бесконечной прокрутки
window.onscroll = async function() {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
const { value } = await getPosts.next();
if (value) {
displayPosts(value);
}
}
};

// Первоначальное получение данных
(async () => {
const { value } = await getPosts.next();
displayPosts(value);
})();

Заключение

Генераторы в JavaScript  —  не просто новшество. Они помогают управлять асинхронными задачами, создавать итерируемые объекты и выполнять многое другое.

Надеюсь, что в следующий раз, когда вам понадобится управлять данными “на лету”, вы без колебаний воспользуетесь генераторами.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Yuri Bett: Don’t Be Afraid of JavaScript Generators

Предыдущая статьяСложные вопросы на собеседовании для тех, кто 7 лет работал с Java. Часть 1
Следующая статьяКлючевые вопросы для собеседования по Spring Boot в 2023 году. Часть 1