Generators and iterators are one of the best feature ES6 gave us. Generators and iterators aren’t completely different things. Infact they are related and work together. Generators are speial type of functions.

A Quick Intro of Generators

At the first glance, generator might look like a function

function *foo() {
    //do some work here
}

Yes, that won’t be wrong, it’s a function but different kind of function called a generator function. Syntax is also quite same. You just need to put a asterik * in order to turn a function into a generator function in either of the following ways

function* foo() { }

function *foo() { }

But how is a generator function different from a normal function

1. Unlike a normal function, a generator function doesn’t work directly but through it’s iterator.
2. Unlike a normal function which follows run to completion process, a generator function can be paused multiple times from inside and resumed from outside. We’ll see how.

3. Unlike a normal function which takes input as arguments on invokation and gives a value out on encountering a ‘return’ statement, a generator function takes values in on every invokation (through it’s iterator) and sends a value out everytime it encounters a ‘yield’ statement. We’ll see later how.
4. To execute generator function, an iterator is required.

Now let’s have a look at an intro example

Declare the Generator
function *foo() {
    yield 1;
    var x = yield 2;
    console.log("x= ",x);
    var y= yield 3;
    return x+y;
}
Instantiate an Iterator of the Generator
//Instantiate an iterator
var fooer= foo();
Start iterating

Use iterator’s next method to iterate over foo, the generator.

fooer.next(); //{value: 1, done: false}
fooer.next(10); //{value: 2, done: false}
fooer.next(20); //x = 20
                //{value: 3, done: false}
fooer.next(10)  //{value: 30, done: true}

Let’s break it down quickly

  • On our first next call, *foo starts executing. In the first line, as soon as it encounters the yield expression, it stops its execution sending out the value (in an object with one more property done indicating whether *foo’s iteration has finshed) corresponding to this yield.
  • On our second next call with argument 10, the point of control reaches exactly where *foo was last stopped at i.e. the first line and the whole yield 1 gets replaced by the 10, the argument that we pass in this next call (although we don’t retian it). Now it’s going to keep running until it encounters another yield which it does in the second line. At this instant, the generator sents out 2 and stops executing.
  • On our third next call, the execution control again gets back to where *foo was last stopped. Now yield 2 gets replaced by the the current argument 20 and gets stored in x. Execution moves to the next line performs a console.log. On coming to the thrid line, the generator again encounters yield, sends 3 out, stops.
  • On our fourth next call similar thing like the one in line 1 happens, *foo encounters a return statement and sends x+y (20+10) out; this time with done being true which means iteration has now been completed.

Note that if there was no return last value would have been undefined.

Behaviour of loops in Generators

Let’s see how a generator behaves with a for loop

function *bar() {
    for(var i = 0; i < 3; i++) {
        yield i;
   }
}

// {value: 0, done: false}
// {value: 1, done: false}
// {value: 2, done: false}
// {value: undefined, done: true}

So it's just like writing yield 0, yield 1, yield 3 separately.

Note that iterator will expire { done: true } after all the yield ___ expressions have been replaced so if there are n yield's used in our generator, no. of times you need to call next method in order to get done: true is n+1 or it gives done: true on encountering a return keyword.

Want to rerun the generator from the very starting ? Just instantiate an iterator of *foo again.

You can read everything about the Generators post series by Kyle Simpson.

Now I leave it to the image below to make you understand how generators work

Generator Visualization

Call-to-action Step Animations

From Call-to-action Step Animations here, I mean animating an element through a given number of states in multiple steps. The element is animated to it's next state on every action (e.g. a button click) and when the element finishes animating through all the states, bringing it to it's initial state and then re-animating it through the same states again on actions so as to create a loop. So it's one state change per action. Generally we might use a counter to achieve such step-animations

See the Pen qNLqmO by Abhishek Sachan (@abhisack) on CodePen.

Just check the JS tab, you'll find that it's not that readable. You have to keep track of the counter, store frames in a mutable state etc. Still it meets our goal well. But with a generator it gets a lot cleaner

var box= document.querySelector(".box");

function *animator(el, framesListArr, options) {

    for(var i=0; i < framesListArr.length; i++) {
        yield animate(el, framesListArr[i], options);
    }

}

var animateBox= animator(box, [
    [ {transform: "scale(1)"}, {transform: "scale(1.5)"} ],
    [ {width: "100vw", height: "100vh"} ]
    ], {
        duration: 100,
        fill: "forwards"
    });

Actually, the arguments you pass at the time of instantiating the generator's iterator are available throughout the same as you'd expect, for all next calls. Behind the scenes, they are actually stored as closure variables for the next method. In our case we pass the element supposed to be animated, arrary of frames list and and object of animation settings.

Now, on performing every next call animate gets called with different frames list out of the array we passed while instantiating until the iterator expires. So, to step forward our animation we perform next calls one after the other delayed by as mush of the time as we wish.

//At some time
animateBox.next(); // { value: Animation, done: false }

//At some other time in future
animateBox.next(); // { value: Animation, done: false }

//At a time
animateBox.next(); // { value: Animation, done: true }

//On clicking the button

btn.addEventListener("click", function() {
    var boxAnimation= animateBox.next();
}, false);

Here, on every 'next' call, eveytime yield is encountered, animate() is called with the new frame list passed to it. Here, we don't care of sending the values out and in becasue only what we want our generator to do is to call animate with new frame list on every next call and then stop until the next 'next call'.

Just see, how readable animateBox.next() looks. Also now we aren't forced to store the array of frame list in a mutable state; instead you pass that array to animator() while instantiating the iterator.

Looping

Instead of relying on the counter like in the last demo, we can check whether the all the frames of have been animated by checking for {done: true}

btn.addEventListener("click", function() {
    var boxAnimation= animateBox.next();
    
    // Reinstantiate the iterator if boxAnimation has finished, 
    // plus cancel all the current animations in order to 
    // bring 'box' to it's initial state

    if(boxAnimation.done) {
        animateBox= animator(box, [
            [ {transform: "scale(1)"}, 
              {transform: "scale(1.5)"} ],
            [ {width: "100vw", height: "100vh"} ]
        ], { duration: 100,
             direction: "forwards"
        });
        cancelAnimations(box);
    }
}, false);

But, we don't need different frames, so why should we repass the same frames as arguments to the animate() again?

Let's create and utilize a function to be able to pass frames once and reinstantiate the iterator with the same frames again.

//returns a function which calls 'animator()'
 //with it's (setAnimator's) own arguments

function setAnimator() {
    var args= [].slice.call(arguments);
    return function() {
        return animator(...args);
    }
}

var boxAnimator= setAnimator(box, [
        [ {transform: "scale(1)"}, 
          {transform: "scale(1.5)"} ],
        [ {width: "100vw", height: "100vh"} ]
        ], { duration: 100,
             fill: "forwards"
        });

var animateBox= boxAnimator();

Now anytime, calling boxAnimator() instantiates animator's iterator with the same arguments

btn.addEventListener("click", function() {
    var boxAnimation= animateBox.next();
        
    // Reinstantiate the iterator if boxAnimation has finished, 
    // plus cancel all the current animations in order to 
    // bring 'box' to it's initial state

    if(boxAnimation.done) {
        animateBox= boxAnimator();
        cancelAnimations(box);
    }
    
}, false);

Now that way we can step-animate any element

var el= document.querySelector(".el");

var elAnimator= setAnimator(el, [
    [ //frame list 1],
    [ //frame list 2],
    [ //frame list 3],
    /*......*/,
    /*......*/,
    /*......*/,
    [ //frame list n]
], {
  duration: 100,
  direction: forwards
}),

animateEl= elAnimator();

Now make your next calls anyway and from any part of your application to progress your animation. And just make sure you reinstantiate the iterator when it gets expired (and calcel the animtions) so that generator is ready to work again from the beginning.

Switching to Animating Other Elements

Utlizing the feature that a generator on it's way to completion can delegate to another generator, while we're on our way to step-animating one element, we can switch to step-animating the other element. After second element finishes step-animating, first element's step-animation resumes. Consider the following code

function *animator(el, framesListArr, options) {

    for(var i=0; i < framesListArr.length; i++) {
        if(i === framesListArr.length/2) {
        
            //delegate the control to animator2()
            yield animator2();          
        }
        yield animate(el, framesListArr[i], options);
    }
}

function *animator2() {
    yield animate(someEl, framesList1, options);
    yield animate(someEl, framesList2, options);
    yield animate(someEl, framesList3, options);
    yield animate(someEl, framesList4, options);
}

var animateBox= boxAnimator();

animateBox.next();
animateBox.next(); 
//delegate control to *animator2() => animate someEl with frameList1=> stop => wait for next next call

And next three next calls will animate someEl and in the fourth next call control will return back to the *animator() which will handle rest of the animations of the box.

If we needed to stop animating someEl and start animating another element; we could delegate to another generator (say *animator3()). Likewise we could form a whole chain of animations.

Here is another demo making use of the code we've just discussed

See the Pen Pen for the Post on Devstreak.com by Abhishek Sachan (@abhisack) on CodePen.

Benefit of using Generators

To animate things in steps using generators, so we don't have to rely on the hacks. We just structure a generator according to our needs, inistantiate an iterator of it with a list of frames & other settings and we're ready to animate with next calls.

Such a fast management for this article's topic would be very hard to achieve without generators. Here, we very smartly avoid some extra works like checking if all the animations on an element have finished and then reinstantiating them. Generators provide us with all the mechanisms, we'd have otherwise needed to code manually which might not be as keen and performant (of course) as generators.

Have you tried making call-to-action step animations with generators? Did it performed better? Share your thoughts and answers below

Share on Facebook Tweet on Twitter Post on Google+ Buffer