JavaScript, a language that has been consistently evolving, introduced a powerful feature in its ES6 (ECMAScript 2015) iteration: Generators. While they might seem daunting at first, generators are invaluable tools for handling asynchronous operations and creating custom iterable sequences. Let's unwrap the mystique behind JavaScript generators.
What Are Generators?
Generators are special functions in JavaScript that allow you to yield (or produce) multiple values on a per-request basis. They pause their execution when yielding a value and can resume from where they left off. This "pausing" capability makes generators versatile for many scenarios, particularly asynchronous tasks.
Basic Syntax of Generators
Generators are defined similarly to regular functions but with an asterisk (*
). The yield
keyword is used to produce a sequence of values.
function* myGenerator() {
yield 'first value';
yield 'second value';
yield 'third value';
}
Using a Generator
To use a generator, you must first call it, which returns a generator object:
const gen = myGenerator();
This object follows the iterator protocol and has a next()
method:
console.log(gen.next()); // { value: 'first value', done: false }
console.log(gen.next()); // { value: 'second value', done: false }
console.log(gen.next()); // { value: 'third value', done: false }
console.log(gen.next()); // { value: undefined, done: true }
Benefits of Generators
Unlike traditional functions that might build and return a huge array, generators produce values on the fly. This means you're not storing large data structures in memory.
Together with Promises, generators offer a smoother way to handle asynchronous operations. This synergy gave birth to async/await, which is essentially syntactic sugar over generators and promises.
Beyond producing a sequence of values, generators can be used to define custom iteration behaviors.
Yielding Other Generators
Generators can yield other generators, making them composable:
function* generatorA() {
yield 'A1';
yield 'A2';
}
function* generatorB() {
yield* generatorA();
yield 'B1';
}
const genB = generatorB();
console.log(genB.next()); // { value: 'A1', done: false }
Generators and Error Handling
You can handle errors in generators with try-catch blocks. If an error is thrown inside a generator, it will set the done
property of the generator to true
.
function* errorGenerator() {
try {
yield 'all good';
throw new Error('Problem occurred');
} catch (err) {
yield err.message;
}
}
Real-world Use Cases
Fetching chunks of data lazily, such as paginated API results or reading large files in segments.
Generating infinite sequences, like an endless series of unique IDs.
Pausing and resuming functions, allowing for more complex flow control.
Generators offer an alternative and often cleaner approach to handling asynchronous operations and generating sequences in JavaScript. While they've been somewhat overshadowed by the rise of async/await, understanding generators gives a deeper insight into the language's capabilities. With generators in your JS toolkit, you're better equipped to tackle a wider range of programming challenges.
Example: Lazy Loading of Data
Imagine you have an application that needs to load large sets of data from a server, like a list of products in an e-commerce site. Instead of loading all data at once, which can be inefficient and slow, you can use a generator to lazily load the data as needed.
Scenario:
- You have an API endpoint that returns a list of products.
- The API supports pagination, allowing you to fetch a limited number of products per request.
- You want to display these products in batches on the user interface, loading more as the user scrolls.
JavaScript Generator Implementation:
function* dataFetcher(apiUrl, pageSize) {
let offset = 0;
let hasMoreData = true;
while (hasMoreData) {
const url = `${apiUrl}?limit=${pageSize}&offset=${offset}`;
yield fetch(url)
.then(response => response.json())
.then(data => {
if (data.length < pageSize) {
hasMoreData = false;
}
offset += data.length;
return data;
});
}
}
// Using the generator
const pageSize = 10;
const apiUrl = 'https://api.example.com/products';
const loader = dataFetcher(apiUrl, pageSize);
function loadMore() {
const nextBatch = loader.next();
nextBatch.value.then(products => {
// Update UI with new batch of products
});
}
// Initial load
loadMore();
// Subsequent loads, e.g., triggered by scrolling
window.addEventListener('scroll', () => {
// Load more data when the user scrolls down
loadMore();
});
Example: Multi-Step Form Navigation
In a web application, you might have a multi-step form where the user needs to complete several steps in sequence. Using a generator, you can create a smooth and controlled navigation flow through these steps.
Scenario:
- The application has a form divided into multiple steps (e.g., personal details, address information, payment details).
- The user should be able to move forward and backward between steps.
- The application should keep track of the current step and display it accordingly.
JavaScript Generator Implementation:
function* formWizard(steps) {
let currentStep = 0;
while (true) {
const action = yield steps[currentStep];
if (action === 'next' && currentStep < steps.length - 1) {
currentStep++;
} else if (action === 'prev' && currentStep > 0) {
currentStep--;
}
}
}
// Define the steps of the form
const steps = ['Step 1: Personal Details', 'Step 2: Address Information', 'Step 3: Payment Details'];
// Create an instance of the form wizard
const wizard = formWizard(steps);
// Function to move to the next step
function nextStep() {
wizard.next('next').value.then(currentStep => {
// Update UI to show the current step
});
}
// Function to move to the previous step
function prevStep() {
wizard.next('prev').value.then(currentStep => {
// Update UI to show the current step
});
}
// Initial step
nextStep();
Top comments (7)
A real useful/clever application of this would have made this article stand out for its uniqueness. Thank you anyway. :)
Thanks for the feedback. I have added a couple of real-world examples, one of which is taken from an application we've actually built in the past. Hopefully this helps illustrate the practicalities a little better
Thanks for the article.
However, once again I haven't seen a practical use.
Show an example in a real application.
Hi. Thanks for the feedback. I have added a couple of real world examples (one taken from a live production application), in addition to the real world use-case list.
in Brave console
note that
done: true
is the one after the last value!same in case of error:
Thanks for the article, it has valuable information that, at least to me, was unknown. I'll give it a try in my next project.