Building an Iterator from a Generator
generatorFunction variable is assigned a generator function. Generator functions are denoted using
Calling a generator function returns an iterator object.
Advancing the Generator
next() method is used to advance the execution of the generator body:
next() method returns an object that indicates the progress of the iteration:
done property indicates that the generator body has been run to the completion.
The generator function is expected to utilize
yield suspends execution of a generator and returns control to the iterator.
When suspended, the generator does not block the event queue:
Pass a Value To the Iterator
yield keyword can pass a value back to the iterator:
Any data type can be yielded, including functions, numbers, arrays and objects.
When the generator is advanced to the completion, the
return value is returned.
Receive a Value From the Iterator
yield keyword can receive a value back from the iterator:
There is no
yield expression to receive the first value "foo". The value is tossed-away.
Understanding the Execution Flow
The best way to understand the execution flow of the generators is to play around using a
debugger. I have illustrated the example that I have used to wrap my head around the I/O order.
Iterating Using the
- The iteration will continue as long as
for..ofloop cannot be used in cases where you need to pass in values to the generator steps.
for..ofloop will throw away the
yield*operator delegates to another generator.
Delegating a generator to another generator is in effect the same as importing the body of the target generator to the destination generator. For illustration purposes only, the above code unfolds to the following:
In addition to advancing the generator instance using
next(), you can
throw(). Whatever is thrown will propagate back up into the code of the generator, i.e. it can be handled either within or outside the generator instance:
Any data type can be thrown, including functions, numbers, arrays and objects.
What Problem Do Generators Solve?
Multiple asynchronous operations one after another produce nesting that is hard to read.
There are several solutions to address the issue, such as using promises or generators. Using generators, the above code can be rewritten as such:
To execute the generator, we need a controller. The controller needs to fulfill the asynchronous requests and return the result back.
The last step is to curry the asynchronous functions into functions that take a single parameter (the callback). This allows to iterate the generator instance knowing that
yield expression is always expecting a singe parameter, the callback that is used to further advance the iteration.
The end result is a script without too many levels of nested callbacks and achieved line independence (the code for one operation is no longer tied to the ones that come after it).
It is common to handle the error handling for each individual asynchronous operation, e.g.
In the following example, I enable the controller to throw an error and use
try...catch block to capture all errors.
Notice that the execution was interrupted before
curry(foo, 'c') was called.
Libraries To Streamline Generator Based Flow-Control
There are several existing libraries that implement a variation of the above controller, as well as offer interoperability with promises, trunks and other techniques.
Exploring ES6 has a chapter about Generators. Axel Rauschmayer write up about generators covers a lot more than I managed to cover in this article. It is a lengthy read, though I thoroughly recommend it.