Не бойтесь JavaScript-генераторов
Я ежедневно просматриваю код различных разработчиков, но редко сталкиваюсь с генераторами.
Почему так?
Неужели люди их не понимают? Или они не видят их преимуществ?
В JavaScript, известном своей гибкостью и широким спектром возможностей, в ECMAScript 2015 появился уникальный инструмент — генераторы. Это мощные средства для управления асинхронным программированием, создания итерируемых объектов и выдачи нескольких значений. В этом руководстве мы рассмотрим механизм работы генераторов, их применение и способы использования их потенциала.
Что такое генераторы
Генераторы отличаются от традиционных функций. Они могут начинать и останавливать своё выполнение несколько раз. Это позволяет им выдавать множество значений и продолжать их выполнение в дальнейшем, что делает их идеальными для управления асинхронными операциями, построения итераторов и работы с бесконечными потоками данных.
Генератор отличается синтаксисом 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); // Outputs: "Start task"
 console.log(tasks.next().value); // Outputs: "Continue task"
 tasks.throw(new Error('Oops! Something went wrong.'));
 // Выводит: "A problem occurred: Oops! Something went wrong."
 console.log(tasks.next()); // Outputs: { value: undefined, done: true }
В приведённом примере после инициирования нескольких задач с помощью метода 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 является оптимальным решением для простых асинхронных задач, генераторы, обладая расширенными возможностями, обеспечивают универсальность.
- Композиция генераторов: Позволяет плавно объединять несколько генераторов, создавая сложные последовательности значений. - function* generateSequence() {
 yield* generateNumbers();
 yield* generateCharacters('A', 'Z');
 }
- Бесконечные генераторы: Генераторы могут создавать бесконечные последовательности значений, что идеально подходит для непрерывных потоков данных или бесконечных алгоритмов. Помните приведённый выше - while(true)?
Сценарий реального мира: Бесконечная прокрутка
Может показаться, что концептуальное и реальное применение генераторов 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 — это не просто новинка, они играют важную роль в управлении асинхронными задачами, создании итерируемых объектов и т.д.
Надеюсь, что в следующий раз, когда вам понадобится управлять данными "на лету", вы без колебаний воспользуетесь генераторами.
Поделитесь, если вы эффективно использовали генераторы в реальных сценариях. Чем больше примеров мы увидим, тем легче будет определить ситуации, в которых они будут уместны.