Iteration allows us to navigate data efficiently, especially when dealing with asynchronous data that arrives on demand, such as in the case of partial downloads. Generators make this task even more convenient by providing an elegant way to pause and resume the execution of functions.
Today, we will learn a little more about an extremely interesting feature that can take you to another level in JavaScript!
What are Iterators and Iterable objects?
Iterable objects are a generalization of arrays, and this generalization allows us, for example, to use objects in a for .. of
loop.
Arrays are iterable, and this makes our lives much easier, but, only arrays? No! In fact, any object that represents a collection (lists, sets, vectors) can be a great candidate for iteration, such as strings.
for (const a of 'any'){
console.log(a)
}
// console log: a
// console log: n
// console log: y
How to use and use case: Pagination
Knowing this, how could we implement pagination using iterators?
class Paginator {
constructor(items, itemsPerPage) {
this.items = items;
this.itemsPerPage = itemsPerPage;
this.currentPage = 0;
}
[Symbol.iterator]() {
return {
items: this.items,
itemsPerPage: this.itemsPerPage,
currentPage: this.currentPage,
next() {
if (this.currentPage * this.itemsPerPage >= this.items.length) {
return { done: true };
}
const start = this.currentPage * this.itemsPerPage;
const end = start + this.itemsPerPage;
this.currentPage++;
return { value: this.items.slice(start, end), done: false };
}
};
}
}
To create an object that takes advantage of iterable implementation, this object must implement the [Symbol.iterator]
method. This method will allow our class to be iterated using an iteration structure like for .. of
.
To implement this method, we must return the next()
method, which is necessary because it defines how the iteration should proceed in a sequence of elements. Each call to next
must return an object with two properties:
done
: A boolean that indicates whether the iteration is complete.value
: The current value of the iteration (ifdone
isfalse
).
And Async Iterators?
Async iterators are an extension of the iterator concept, allowing iteration over asynchronous data. They are particularly useful when you need to handle I/O operations or network requests that return data asynchronously.
To implement an Async Iterator:
- Use
Symbol.asyncIterator
instead ofSymbol.iterator
. - The
next()
method must return aPromise
.- Just implement the method as
async next()
.
- Just implement the method as
- To iterate over such an object, we must use a
for await (let item of iterable)
loop.- Here, we only change the word
await
.
- Here, we only change the word
When implementing an async iterator, the spread operator
...
does not work. It only uses iterators.
What are Generators?
Generators are simply a way to implement iterators outside of a class, like a function.
Generators are created using an asterisk when defining a function function*
and use yield
to generate values, and thus we can use them in a for .. of
loop just like iterators.
function* fibonacciGenerator() {
let a = 0, b = 1;
while (true) {
yield a; // Returns the current Fibonacci number
[a, b] = [b, a + b]; // Calculates the next number in the Fibonacci sequence
}
}
And here, the use can be the same as iterators! Both using the next
method and in a for .. of
loop (not exactly this Fibonacci generator because it never ends, huh!)
const fibo = fibonacciGenerator()
console.log(fibo.next()) // {value: 0, done: false}
console.log(fibo.next()) // {value: 1, done: false}
console.log(fibo.next()) // {value: 2, done: false}
console.log(fibo.next()) // {value: 3, done: false}
console.log(fibo.next()) // {value: 5, done: false}
And Async Generators?
Async Generators are a powerful combination of generators and async iterators. They allow you to define functions that can pause execution (yield
) and, at the same time, perform asynchronous operations. This is extremely useful for scenarios where you need to process asynchronous data in parts, such as consuming paginated APIs or continuous data streams.
To implement an async generator:
- Use
async function*
to define an async generator. - Use
await
within the function body andyield
to pause execution. - Use
for await...of
to iterate over the generated values.
Real-life example: All records from a paginated API
Imagine you are consuming a paginated API that returns a set of results on each page, but for some reason, in your use case, you need to return all possible values.
async function* fetchPagedData() {
let currentPage = 1;
let hasMoreData = true;
while (hasMoreData) {
const response = await fetch(`https://example.com/?page=${currentPage}`);
const data = await response.json();
if (data.items.length === 0) {
hasMoreData = false;
} else {
yield data.items;
currentPage++;
}
}
}
To use your async generator:
const pageContents = [];
for await (const items of fetchPagedData(apiEndpoint, pageSize)) {
pageContents.push(...items);
}
This way, you would have all the items in an extremely elegant manner!
Although the applications of async generators are rare, they can be used in moments such as data streaming, real-time feeds, and large data processing.
So, even if you don't need to use them today, being aware of these techniques is essential so you know what to do when you need to, thus elevating your JavaScript skills to a new level!