Generators in JavaScript are one of the concepts that took me a while to get my head around and to fully understand its power and usages. In this post, I will walk you through a simple explanation of how Generators work and some practical uses of it ( How to create an infinite loop without crashing your application for instance )
What is a generator?
First, let us talk about the broad idea of what generators are. The way I understood it generators are a special type of function that does not return a value but instead it returns multiple values whenever you ask for them.
Generators can be imagined as a way to stop the execution of a function at a specific point and retrieve the output and then continue the execution. What makes the execution stops is a special keyword called yield
so whenever this keyword is found this means that a value is being generated by the generator function.
Let us look at a simple example.
function* basicGenerator() {
yield 1;
yield 2;
yield 3;
return 4;
}
Here we defined our generator function, whenever we want to create a generator function we have to provide an asterisk after the function keyword.
If we thought about the output of such a function we would probably say that it will output 4, but here comes the tricky part, generators return a generator object which looks like // [object Generator]
This object will be responsible for the execution of the function.
function* basicGenerator() {
yield 1;
yield 2;
yield 3;
return 4;
}
let generator = basicGenerator();
let firstResult = generator.next();
console.log(firstResult);
// {value: 1, done: false}
Here we executed the basicGenerator()
function and it returned a generator object and we logged its output. The Generator object contains three main functions, a next()
to continue the execution and returns an object of value and done ( will discuss it in a moment ) and a throw()
that stops the generator's execution and throws an error and a return()
that finishes the execution the return a value.
Let's first look at how the next()
function works, when we execute it the generator function will point to the next execution level or the next yield keyword and will return a value of the previous yield keyword. So in the above code the first next()
will return {value: 1, done: false}
and will point to the next yield that shall return 2 in the next execution.
You might ask what does done
refer to? done will always be true until there are no more yields available for execution or the execution pointed to a return keyword, at that moment any next()
calls shall return an undefined value.
According to what we said above we should understand the output of the code below:
function* basicGenerator() {
yield 1;
yield 2;
yield 3;
return 4;
}
let generator = basicGenerator();
let data = {};
while(!data.done) {
data = generator.next();
console.log(data.value);
}
// [1,2,3,4]
Here we created a while loop that will keep asking for values till the generator returns an indicator done : false
that indicates that there are no more executions available in our generator.
Generators are iterable
Another thing that should be taken into account is that generators are iterable and a for...of
could be used to iterate over the values of a generator like this:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let generator = generateSequence();
for(let value of generator) {
console.log(value);
}
// 1 2 3
Passing values to generators
One of the very handy features in generators is you can actually pass an argument to the generator and it will be read in the execution level that the generator is pointing at. Lets look at an example to further explain this.
function* basicGenerator() {
let res = yield 1;
console.log(res); // Passing This
let res2 = yield 2;
console.log(res2); // Done Passing
yield 3;
}
const generator = basicGenerator();
generator.next();
generator.next("Passing This");
generator.next("Done Passing");
As shown above, now we not just calling the yield
keyword, we are also assigning a variable to its output, and when we call the generator next()
function we first pass no arguments ( the first next is by default will neglect any passed arguments ) and then we pass whatever we want, so the second next will have Passing This
passed to it and thus it will assign this value to the first variable in our execution which is res
and then we pass another argument and res2
shall receive the Done Passing
value.
This could be very handy as now we not just control the execution of our Generators but we also could pass arguments to them and manipulate their behavior accordingly.
Why would we use Generators?
One of the use cases that generators are used for is simulating an infinite loop. For example, if you decided that you want to create an ID generator that starts from 0 till Infinity you would do something like this:
function* infiniteIdGenerator() {
let start = 0;
while (true) yield start++;
}
const generator = infiniteIdGenerator();
generator.next(); // 0
generator.next(); // 1
generator.next(); // 2
generator.next(); // 3
// ...Infinity
And now you could generate a new ID whenever you want and it will be guaranteed that it will be a uniquely created one.
Another use case is throttling, throttling is basically delaying the execution of some code or function.
export function* throttle(func, time) {
let timerID = null;
function throttled() {
clearTimeout(timerID);
timerID = setTimeout(func.bind(window, arg), time);
}
while (true) throttled(yield);
}
const generator = throttle(() => console.log("some logic"), 300);
generator.next();
Conclusion
I explained some of the core concepts of generators today but actually, there are way more than I said today, there are other ways generators are used like generators composition, and even it is used in one of the most famous redux libraries, redux-saga
that allows it to create side-effects with generators.
Top comments (0)