The Ember run loop can be a bit of a mystery to all Ember developers, particularly new developers. In most cases, Ember abstracts away all of the tough details about the run loops so we, as Ember developers, don't have to think about it too much. It has even been written that Ember developers don't need to know about the Run loop. This seems a bit short sighted. We are going to try and demystify what the run loop does and how it works.

At it's most basic level, the Ember run loop is a queuing system for all the events triggered by user interactions in your Ember app. The run loop consists of a series of queues that allows you to batch and prioritize your work in a way that makes your app performant and efficient. For example, with the run loop we can make sure that when a user triggers an event, anything observing changes made by that event don't fire twice. We can also make sure that all routerTransitions are able to complete before anything from the render bucket is executed. The Ember run queues are:

  1. "sync",
  2. "actions",
  3. "routerTransitions",
  4. "render",
  5. "afterRender",
  6. "destroy"

There is a decent breakdown of the responsibility of each queue in the Guides, so we wont go into that here. For the most part, Ember handles the run loop stuff for you. When you trigger actions in Ember all the observers, transitions, and computed properties are prioritized appropriately. But, sometimes, you might need to perform some non-Ember operations. In these cases, you will probably need to understand a bit more about the run loop. So, let's look at how it works. We are going to start by looking at an example in the Ember guides. First, let's go to ember twiddle. From there let's create a new component, the default my-component will do fine. We will add our new component to the application template. Now, let's open up the component's JavaScript file. Here we will use the didInsertElement hook to add a listener to a click event on a button element.

import Ember from 'ember';

export default Ember.Component.extend({
  didInsertElement(){
    Ember.$('button').click(()=>{
      console.log('outside of the run loop.');
    });
  }
});

Now, in the components template, let's just add a button:

<button>button</button>

Now, clicking the button will log our message to the console. Ember attempts to incorporate events into it's run loop, but the "auto run loop" is turned off for testing and doesn't always work like we would expect. We can address this by manually creating a run loop. That might look like this:

import Ember from 'ember';

export default Ember.Component.extend({
  didInsertElement(){
    Ember.$('button').click(()=>{
      console.log('outside run loop');
      Ember.run(()=>{
        console.log('inside a run loop.');
      });
      console.log('after run loop');
    });
  }
});

But as we discussed before, the loop is a set of queues. So, we need to be able to schedule code to run in the correct queue. We can do this by calling Ember.run.schedule() and then passing in the queue as the first argument and the callback as the second argument. For example, we can say Ember run schedule render and then log out scheduled for render. Then we can do the same for actions and afterRender. Notice that we have render first here. We know from earlier that render runs after actions so let's just see that in action...

import Ember from 'ember';

export default Ember.Component.extend({
  didInsertElement(){
    Ember.$('button').click(()=>{
      console.log('outside run loop');
      Ember.run.schedule('render', ()=>{
        console.log('scheduled for render');
      });
      Ember.run.schedule('actions', ()=>{
        console.log('scheduled for actions');
      });
      Ember.run.schedule('afterRender', ()=>{
        console.log('scheduled for afterRender');
      });
      console.log('after run loop');
    });
  }
});

Clicking the button produces this output:

outside run loop
after run loop
scheduled for actions
scheduled for render
scheduled for afterRender

Now we have something interesting going on. The code outside of the run loop is obviously not managed while the code in the run loop is scheduled as we would expect. So we can see the queuing system at work. We also might want to prevent duplication, for instance if an computed property is going to fire we only want that to update once, even if multiple keys are changed. We can accomplish something like this using scheduleOnce. It makes sure that we don't call the same function repeatedly in the same queue. To see this, we can just create a function called func1 and scheduleOnce afterRender, but call that twice.

import Ember from 'ember';

var func1 = function(){
  console.log('func1');
}

export default Ember.Component.extend({
  didInsertElement(){
    Ember.$('button').click(()=>{
      console.log('outside run loop');
      Ember.run.schedule('render', ()=>{
        console.log('scheduled for render');
      });
      Ember.run.schedule('actions', ()=>{
        console.log('scheduled for actions');
      });
      Ember.run.scheduleOnce('afterRender', func1);
      Ember.run.scheduleOnce('afterRender', func1);
      console.log('after run loop');
    });
  }
});

There are a few comments in the guides on testing and the run loop that are worth looking at. You will most often run into problems with the run loop in your tests. If you forget to manually create a run loop, Ember will generally try and create one for you. However, that wont happen in your tests. And Ember's tests rely on the run loop the execute some testing hooks. If you get an error that looks something like this: Autoruns are disabled in test mode, you probably just forgot to create a run loop somewhere that you need one.

That is it for today, tomorrow we will very briefly look at observers. See you then.