Не бойтесь 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 — это не просто новинка, они играют важную роль в управлении асинхронными задачами, создании итерируемых объектов и т.д.
Надеюсь, что в следующий раз, когда вам понадобится управлять данными "на лету", вы без колебаний воспользуетесь генераторами.
Поделитесь, если вы эффективно использовали генераторы в реальных сценариях. Чем больше примеров мы увидим, тем легче будет определить ситуации, в которых они будут уместны.