by Eoin Kelly
If you spot any of the (sadly inevitable) errors you would be doing me a great favour by opening an issue :-).
You can get started with Ember application development without understanding the runloop. However at some point you will want to dig in and understand it properly so you can use it skillfully. It is my sincere hope that this handbook can be your guide.
We are about to take a deep dive into the Ember.js runloop. Together we will answer these questions:
This is not reference documentation - the Ember API docs have that nicely covered. This isn't even the "I'm an experienced dev, just give me the concepts in a succinct way" documentation - the Official Ember Run-loop guide has that covered. This is a longer, more detailed look at the runloop.
As you learn more about the Ember runloop you will come to understand that, well, it just isn't very loopish. The name is a bit unfortunate as it implies that there is a single instance of the runloop sitting somewhere in memory looping endlessly running things. As we will see soon this is not true.
In alternate universes the runloop might have been named:
OK some of those names are really terrible (except Runelope of course, that one is pure gold and should be immediately pushed to Ember master). Naming is a hard problem and hindsight is 20/20. The runloop is what we have so that is what we will call it but try not to infer too much about its action from its name.
On our journey to understand the runloop we must first understand the environment it lives in and the problems it is trying to solve. Lets set the scene by refreshing a few fundamentals about how Javascript runs. (If you are an experienced Javascript developer you may want to just skip this part)
Our story begins with when the browser sends a request to the server and the server sends HTML back as a response.
The browser then parses this HTML response. Every time it finds a script it executes it immediately(*) Lets call this the setup phase. This setup phase happens well before the user sees any content or gets a chance to interact with the DOM. Once a script is finished executing the browser never runs it again.
(*) Things like defer
tweak this somewhat but this is a useful simplification.
The browser does most of its communication with Javascript by sending "events". Usually these are created in response to some action from one of:
mousemove
)load
)However there are a few events that the browser generates itself to tell
Javascript about some important event in the lifecycle of the page. The most
widely used of these is DOMContentLoaded
which tells Javascript that the HTML
has been fully parsed and the DOM (the memory structure the browser builds by
parsing the HTML) is complete. This is significant for Javascript because it
does most of its setup work in response to this event.
Javascript is lazy but well prepared! During the setup phase, Javascript prepared its work space (or mise en place if you prefer) - it created the objects it would now need to respond to orders (events) from the browser and also told the browser in detail what events it cares about e.g.
Hey browser, wake me up and run this function I'm giving you whenever the user clicks on an element with an id attribute of
do-stuff
.
The description above makes it look like the browser is the one giving all the orders but the browser is a team player and has a few things it can do to help Javascript get the job done:
Timers. Javascript can use the browser like an alarm clock:
Javascript: Hey browser, wake me up and run this function I'm giving you in 5 seconds please.
Talking to other systems. If Javascript needs to send or receive data to other computers it asks the browser to do it:
Javascript: Hey browser, I want to get whatever data is at
http://foo.com/things.json
please.
Browser: Sure thing but it might take a while. What do you want me to do when it comes back?
Javascript: I have two functions ready to go (one for a successful data fetch and one for a failure) so just wake me up and run the appropriate one when you finish.
Browser: cool.
We usually refer to this talking to other systems stuff as Web APIs e.g.
Javascript can use these services of the browser both during the setup phase and afterwards. For example part of the Javascript response to a "click" event on a certain element might be to retrieve some data from the network and also schedule a timer to do some future work.
We now know enough to see the pattern of how javascript and the browser interact and to understand the two phases:
A solid understanding of this stuff is required to understand the runloop so if you are unclear about any of this and want to dig a little deeper I recommend a wonderful video by Philip Roberts at JSConf EU that goes into the Javascript event loop in more detail. It is a short watch and includes a few "aha!"-inducing diagrams.
Since Ember is Javascript we already know quite a bit about how Ember works:
DOMContentLoaded
event is significant in the life of an Ember app. It tellssetTimeout
)How does your Ember application relate to the Ember framework? The machinery for responding to events is part of Ember framework itself but it does not have a meaningful response without your application code.
For example if the user is on /#/blog/posts
and clicks a link to go to
/#/authors/shelly
the Ember framework will receive the click event but it
won't be able to do anything meaningful with it without:
BlogRoute
, PostsRoute
, AuthorsRoute
The Ember docs have a list of events Ember listens for by default which I have repeated here:
These are the entry points into our code. Whenever Ember code runs after the setup phase, it is in response to an event from this list.
This is a good resource for refreshing your understanding of how DOM events work. To get the most of the following discussion you should be familiar with how the browser propagates events and what the phrases "capturing phase" and "bubbling phase" mean.
Ember registers listeners for these events similarly to how we might do it ourselves with jQuery i.e.
<body>
. If you specify a rootElement
then that will be used instead.The pattern of how Javascript (Ember) works is periods of intense activity in response to some event followed by idleness until the next event happens. Lets dig a little deeper into these periods of intense activity.
We already know that the first code to get run in response to an event is the listener function that Ember registered with the browser. What happens after that?
Lets consider some code from an imaginary simple Javascript app:
http://jsbin.com/diyuj/5/edit?html,js,console,output
This code manages the "Mark all completed" button in the UI.
Click the button a few times and notice the console output. Notice that there are some patterns to the tasks performed:
and that the do work as you find it approach that this app takes causes these different types of work to be interleaved.
The code in this app is obviously very incomplete and I'm sure you can see many ways it could be improved. However there are some problems that might not be obvious at first, problems that you will only start to notice when the app grows in complexity. To understand these lets look at what it is not doing:
Together these problems mean our simplistic Todo app will have serious scaling problems.
We have identified some problems caused by an uncoordinated approach to doing work. How does Ember solve them?
Instead of doing work as it finds it, Ember schedules the work on an internal set of queues. By default Ember has six queues:
console.log(Ember.run.queues);
// ["sync", "actions", "routerTransitions", "render", "afterRender", "destroy"]
Each queue corresponds to a "phase of work" identified by the Ember core team. This set of queues and the code that manages them is the Ember runloop.
You can see a summary of the purpose of each queue in the Runloop Guide but here we are going to focus on the queues themselves.
First lets get some terminology sorted:
How Ember handles events:
Lets consider some subtle consequences of this simple algorithm:
Ember does a full queue scan after each job - it does not attempt to finish a full queue before checking for earlier work.
Ember will only get to jobs on a queue if all the previous queues are empty.
Ember cannot guarantee that, for example, all sync
queue tasks will be
complete before any actions
tasks are attempted because jobs on any queue
after sync
might add jobs to the sync
queue. Ember will however do its
best to do work in the desired order. It is not practical for your app to
schedule all work before any is performed so this flexibility is necessary.
At first glance it may seem that the runloop has two distinct phases
but this is subtly incorrect. Functions that have been scheduled on a runloop queue can themselves schedule functions on any queue in the same runloop. It is true that once the runloop starts executing the queues that code outside the queues cannot schedule new jobs. In a sense the initial set of jobs that are scheduled are a "starter set" of work and Ember commits to doing it and also doing any jobs that result from those jobs - Ember is a pretty great employee to have working for you!
Something that is not obvious from that description is that there is no "singleton" runloop. This is confusing because documentation (including this guide) uses the phrase "the runloop" to refer to the whole system but it is important to note that there is not a single instance of the runloop in memory (unlike the Ember container which is a singleton). There is no "the" runloop, instead there can be multiple instances of "a" runloop. It is true that Ember will usually only create one runloop per DOM event but this is not always the case. For example:
Ember.run
(see below) you will be creating your ownAnother consequence of the runloop not being a singleton is that it does not function as a "global gateway" to DOM access for the Ember app. It is not correct to say that the runloop is the "gatekeeper" to all DOM access in Ember, rather that "coordinated DOM access" is a pleasant (and deliberate!) side-effect of organising all the work done in response to an event.. As mentioned above, multiple runloops can exist simultaneously so there is no guarantee that all DOM access will happen at one time.
From what I have observed, Ember typically runs one runloop in response to each DOM event that it handles.
This repo also contains the noisy runloop kit which is trivial demo app and a copy of Ember that I have patched to be very noisy about what its runloop does. You can add features to the demo app and see how the actions the runloop takes in response in the console. You can also use the included version of Ember in your own project to visualise what is happening there. Obviously you should only include this in development because it will slow the runloop down a lot.
When you start getting the runloop to log its work you will quickly get
overwhelmed by its running in response to mouse events that happen very
frequently on desktop browsers e.g. mousemove
. Below is an initializer for
Ember that will stop it listening to certain events. You probably want to add
this to whatever Ember app you are trying to visualise the runloop for unless
you are actually using mousemove
, mouseenter
, mouseleave
in your app.
/**
* Tell Ember to stop listening for certain events. These events are very
* frequent so they make it harder to visualise what the runloop is doing. Feel
* free to adjust this list by adding/removing events. The full list of events
* that Ember listens for by default is at
* http://emberjs.com/api/classes/Ember.View.html#toc_event-names
*
*/
Ember.Application.initializer({
name: 'Stop listening for overly noisy mouse events',
initialize: function(container, application) {
var events = container.lookup('event_dispatcher:main').events;
delete events.mousemove;
delete events.mouseenter;
delete events.mouseleave;
}
});
Calls to any of
run.schedule
run.scheduleOnce
run.once
have the property that they will approximate a runloop for you if one does not already exist. These automatically (implicitly) created runloops are called autoruns.
Lets consider an example of a click handler:
$('a').click(function(){
console.log('Doing things...');
Ember.run.schedule('actions', this, function() {
// Do more things
});
Ember.run.scheduleOnce('afterRender', this, function() {
// Yet more things
});
});
When you call schedule
Ember notices that there is not a currently open
runloop so it opens one and schedules it to close on the next turn of the JS
event loop.
Here is some pseudocode to describe what happens:
$('a').click(function(){
// 1. autoruns do not change the execution of arbitrary code in a callback.
// This code is still run when this callback is executed and will not be
// scheduled on an autorun.
console.log('Doing things...');
Ember.run.schedule('actions', this, function() {
// 2. schedule notices that there is no currently available runloop so it
// creates one. It schedules it to close and flush queues on the next
// turn of the JS event loop.
if (! Ember.run.hasOpenRunloop()) {
Ember.run.start();
nextTick(function() {
Ember.run.end()
}, 0);
}
// 3. There is now a runloop available so schedule adds its item to the
// given queue
Ember.run.schedule('actions', this, function() {
// Do more things
});
});
// 4. scheduleOnce sees the autorun created by schedule above as an available
// runloop and adds its item to the given queue.
Ember.run.scheduleOnce('afterRender', this, function() {
// Yet more things
});
});
Although autoruns are convenient you should not rely on them because:
run.schedule
, run.scheduleOnce
and run.once
are wrappedWe know that
run.schedule
run.scheduleOnce
run.once
create a new runloop if one does not exist and that these automatically (implicitly) created runloops are called autoruns.
If Ember.testing
is set then this "automatic runloop approximation creation"
behaviour is disabled. In fact when Ember.testing
is set these three functions
will throw an error if you run them at a time when there is not an existing
runloop available.
The reasons for this are:
The Ember runloop API docs are the canonical resource on what each function does. This section will provide a high-level overview of how the API works to make it easier to categorise it in your head and put it to use.
In the API we have:
Ember.run
Ember.run.schedule
Ember.run.scheduleOnce
Ember.run.once
Ember.run.later
Ember.run.next
Ember.run.debounce
Ember.run.throttle
Ember.run.cancel
Ember.run
Ember.run.begin
Ember.run.end
Ember.run.sync
Function | Which runloop? | Which queue? | Creates new runloop? | Notices Ember.testing ? |
Runs callback in current JS event loop turn? |
---|---|---|---|---|---|
Ember.run |
always-new | actions |
Always | No | Yes |
Ember.run.debounce |
always-new | actions |
Always | No | No |
Ember.run.throttle |
always-new | actions |
Always | No | No |
Ember.run.join |
current | actions |
If required | No | Yes |
Ember.run.bind |
current | actions |
If required | No | No |
Ember.run.schedule |
current | chosen by param | If required | Yes | Yes |
Ember.run.scheduleOnce |
current | chosen by param | If required | Yes | Yes |
Ember.run.once |
current | actions |
If required | Yes | No |
Ember.run.later |
future | actions |
If required | Yes | No |
Ember.run.next |
future | actions |
If required | Yes | No |
Ember.run.begin |
NA | NA | Never | No | NA |
Ember.run.end |
NA | NA | Never | No | NA |
Ember.run.cancel |
NA | NA | NA | NA | NA |
Ember.run.sync |
NA | NA | NA | NA | NA |
Legend:
actions
There are two functions in the runloop API that let us schedule "future work":
Ember.run.later
Ember.run.next
Each of these API functions is a way of expressing when you would like work (a callback function) to happen. The guarantee provided by the runloop is that it will also manage the other work that results from running that function. It does not guarantee anything else!
The key points:
[(timestamp, fn), (timestamp, fn) ... ]
(timestamp, callback)
pair to this array.Ember.run
) and schedules eachactions
queue.Consequences:
Ember.run
yourself.actions
queue. If you need to runactions
queue callback.Ember provides two flavors of rate control.
Ember.run.debounce
Ember.run.throttle
These functions are useful because they allow us to control when the given
callback is not run. When it is actually run, these functions use Ember.run
so these functions can be thought of as "Ember.run
with some extra controls
about when the function should be run"
It can take a while to get our heads around the subtleties of the runloop. In exchange we get the performance and scaling benefits that the runloop provides. I hope that you now feel more equipped to use the runloop skillfully.
Happy hacking.
The primary documentation for the Ember runloop is Official Ember Run-loop guide and the Ember API docs
These are other sources I studied in compiling this guide: