This blog is intended for developers who are familiar with JavaScript development and wants to learn about generator, a special function introduced as part of ECMAScript 6 release and tends to play a key role in redux-saga, a redux-based middleware library to implement asynchronous data flow in Redux.
Note: The crux of this blog is about generator functions and serves as a prelude to my upcoming blog on redux-saga so discussion pertaining to redux-saga will not be made in this blog.
What are generators?
According to MDN,
“Generators are functions that can be exited and later re-entered. Their context (variable bindings) will be saved across re-entrances.”
Consider generators as special functions in JavaScript that do not adhere to the same rules that regular functions need to follow. It is much easier to understand generators by looking at an example and see how they work.
How are they defined?
Generators, when declared via function declaration, they can be declared by attaching an *
(asterisk) as a suffix to the function keyword or as a prefix to the function name. Or when declared via function expression, they can be declared by attaching an *
(asterisk) as a suffix to the function keyword.
Note: If you are unsure about the difference between function declaration and function expression, please read my blog here: https://ahamedblogs.wordpress.com/2020/02/12/function-expression-vs-function-declaration/
Here are the possible ways they can be declared:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function* generator() { | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function *generator() { | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const generator = function*() { | |
} |
How do they work?
Here are some of the examples to gradually understand generators:
Return value
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function *generator() { | |
return 'value'; | |
} | |
console.log(generator()); //=> Object [Generator] {} |
The return value of a generator function is always a generator object. In the example above, invoking the generator function returned a generator object instead of the custom return value.
Exit and Reenter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function *generator() { | |
yield 'value1'; | |
console.log('I am here'); | |
yield 'value2'; | |
} | |
const g = generator(); //=> Object [Generator] {} | |
console.log(g.next()); //=> {value: "value1", done: false} | |
console.log(g.next()); | |
/** | |
* I am here | |
* {value: "value2", done: false} | |
**/ | |
console.log(g.next()); //=> {value: undefined, done: true} | |
console.log(g.next()); //=> {value: undefined, done: true} |
Keywordyield
used within a generator’s body and next
method called on the returned generator object help shape the behavior of generator functions. Its behavior can be understood based on the above example:
- Calling the
generator
directly will not execute the function body, rather it returns a generator object. - When
next
method is invoked on this generator object, then thegenerator
‘s body is executed until the firstyield
expression. This expression returns a generator object withvalue
which contain the value yielded anddone
which indicates whether all generator has yielded its last value. In the example, the expression produceddone
flag as false since the generator has not finished its yield. - Calling the
next
method again on the generator object will resume the generator’s execution(instead of executing the body from the beginning) which then will execute from the last executedyield
expression, producing again a generator object. Note that thedone
flag of this object is stillfalse
. - Any subsequent calls to
next
will not execute the generator function and simply return generator objects withundefined
yield values and finally marks the completion of the generator function as indicated by thedone
flag since there are no moreyield
expressions left to execute.
Finishing yield with return
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function *generator() { | |
yield 1; | |
return null; | |
yield 2; | |
} | |
const g = generator(); | |
console.log(g.next()); //=> {value: 1, done: false} | |
console.log(g.next()); //=> {value: null, done: true} | |
console.log(g.next()); //=> {value: undefined, done: true} |
The generator function is complete when it no longer has anyyield
expressions left to execute or when it recognizes a return statement. But note in the above example, executing the next
method the second time has yielded null
which is same as the return value of the generator instead of undefined
like in the previous example.
Preserving context
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function *generator(i) { | |
yield i; | |
yield i + 1; | |
} | |
const g = generator(10); | |
console.log(g.next()); //=> {value: 10, done: false} | |
console.log(g.next()); //=> {value: 11, done: false} |
One of the coolest features about generators is its ability to remember the context across executions. In this example, when calling the next
method on the generator object, the function remembers the initial argument value,10
and calculations are made on it.
Why was it introduced?
ECMAScript originally introduced generators to write asynchronous code with Promise as if they are synchronous mainly because at that time, handling asynchronous tasks was complicated as the code became unmanageable due to callback hell. If you are wondering how we can write asynchronous code with generators, here is the code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const AsyncModule = generator => { | |
return () => { | |
const iterator = generator(); | |
const resolve = next => { | |
if (next.done) { | |
return Promise.resolve(next.value); | |
} | |
return Promise.resolve(next.value).then(response => { | |
//recursively call resolve() until done is `true` | |
return resolve(iterator.next(response)); | |
}); | |
}; | |
return resolve(iterator.next()); | |
}; | |
}; | |
const asyncGenerator = function*() { | |
const response = yield fetch('https://pokeapi.co/api/v2/pokemon'); | |
const json = yield response.json(); | |
return json; | |
}; | |
const getData = AsyncModule(asyncGenerator); | |
console.log(getData().then(d => console.log(d))); =>//prints api data |
Note: Handling asynchronous tasks in JavaScript is much easier with Async/Await now. Please check it out here: https://ahamedblogs.wordpress.com/2020/02/16/async-await-in-javascript/
Conclusion
We have learned about generator functions and understood how they work. Generators really pack a lot of features beyond just handling async code and it is a great addition to the language. Do not worry if you do not understand the use cases of generators as of now. It will make sense when we look at its practical application in redux-saga. Happy coding!