Ответьте честно на вопрос: часто ли вы сталкиваетесь с кодом, в котором используются генераторы? Я ежедневно просматриваю код различных разработчиков, но редко встречаю генераторы.
Почему так происходит? Разработчики не знают, как их использовать? Или не ценят их преимуществ?
Генераторы были представлены в 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 — не просто новшество. Они помогают управлять асинхронными задачами, создавать итерируемые объекты и выполнять многое другое.
Надеюсь, что в следующий раз, когда вам понадобится управлять данными “на лету”, вы без колебаний воспользуетесь генераторами.
Читайте также:
- Создание приложения для отслеживания фильмов с помощью HTML, CSS и JavaScript
- 5 недооцененных возможностей JavaScript
- 8 продвинутых вопросов для собеседования по JavaScript
Читайте нас в Telegram, VK и Дзен
Перевод статьи Yuri Bett: Don’t Be Afraid of JavaScript Generators