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

Источник: «Don't Be Afraid of JavaScript Generators»
Давайте будем честными: как часто мы сталкиваемся с кодом, использующим генераторы?

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

Почему так?

Неужели люди их не понимают? Или они не видят их преимуществ?

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

Что такое генераторы

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

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

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

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

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

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

Давайте разберём объект Generator:

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

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

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

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

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

Синхронные и асинхронные итерации с генераторами

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

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

Расширенное применение генераторов

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

Сценарий реального мира: Бесконечная прокрутка

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

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

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

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

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

// CSS code
<style>
.post {
height: 300px;
}
</style>
// HTML code
<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; // end of data
}

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 — это не просто новинка, они играют важную роль в управлении асинхронными задачами, создании итерируемых объектов и т.д.

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

Поделитесь, если вы эффективно использовали генераторы в реальных сценариях. Чем больше примеров мы увидим, тем легче будет определить ситуации, в которых они будут уместны.

Дополнительные материалы

Предыдущая Статья

Новое в Symfony 6.4: Контексты сериализатора на основе классов

Следующая Статья

События в Laravel