Managing Asynchrony Edit Page


Many Ember concepts, like bindings and computed properties, are designed to help manage asynchronous behavior.

Without Ember

We'll start by taking a look at ways to manage asynchronous behavior using jQuery or event-based MVC frameworks.

Let's use the most common asynchronous behavior in a web application, making an Ajax request, as an example. The browser APIs for making Ajax requests provide an asynchronous API. jQuery's wrapper does as well:

1
2
3
4
jQuery.getJSON('/posts/1', function(post) {
  $("#post").html("<h1>" + post.title + "</h1>" +
    "<div>" + post.body + "</div>");
});

In a raw jQuery application, you would use this callback to make whatever changes you needed to make to the DOM.

When using an event-based MVC framework, you move the logic out of the callback and into model and view objects. This improves things, but doesn't get rid of the need to explicitly deal with asynchronous callbacks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Post = Model.extend({
  author: function() {
    return [this.salutation, this.name].join(' ')
  },

  toJSON: function() {
    var json = Model.prototype.toJSON.call(this);
    json.author = this.author();
    return json;
  }
});

PostView = View.extend({
  init: function(model) {
    model.bind('change', this.render, this);
  },

  template: _.template("<h1><%= title %></h1><h2><%= author %></h2><div><%= body %></div>"),

  render: function() {
    jQuery(this.element).html(this.template(this.model.toJSON());
    return this;
  }
});

var post = Post.create();
var postView = PostView.create({ model: post });
jQuery('#posts').append(postView.render().el);

jQuery.getJSON('/posts/1', function(json) {
  // set all of the JSON properties on the model
  post.set(json);
});

This example doesn't use any particular JavaScript library beyond jQuery, but its approach is typical of event-driven MVC frameworks. It helps organize the asynchronous events, but asynchronous behavior is still the core programming model.

Ember's Approach

In general, Ember's goal is to eliminate explicit forms of asynchronous behavior. As we'll see later, this gives Ember the ability to coalesce multiple events that have the same result.

It also provides a higher level of abstraction, eliminating the need to manually register and unregister event listeners to perform most common tasks.

You would normally use ember-data for this example, but let's see how you would model the above example using jQuery for Ajax in Ember.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
App.Post = Ember.Object.extend({

});

App.PostController = Ember.ObjectController.extend({
  author: function() {
    return [this.get('salutation'), this.get('name')].join(' ');
  }.property('salutation', 'name')
});

App.PostView = Ember.View.extend({
  // the controller is the initial context for the template
  controller: null,
  template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>")
});

var post = App.Post.create();
var postController = App.PostController.create({ model: post });

App.PostView.create({ controller: postController }).appendTo('body');

jQuery.getJSON("/posts/1", function(json) {
  post.setProperties(json);
});

In contrast to the above examples, the Ember approach eliminates the need to explicitly register an observer when the post's properties change.

The {{title}}, {{author}} and {{body}} template elements are bound to those properties on the PostController. When the PostController's model changes, it automatically propagates those changes to the DOM.

Using a computed property for author eliminated the need to explicitly invoke the computation in a callback when the underlying property changed.

Instead, Ember's binding system automatically follows the trail from the salutation and name set in the getJSON callback to the computed property in the PostController and all the way into the DOM.

Benefits

Because Ember is usually responsible for propagating changes, it can guarantee that a single change is only propagated one time in response to each user event.

Let's take another look at the author computed property.

1
2
3
4
5
App.PostController = Ember.ObjectController.extend({
  author: function() {
    return [this.get('salutation'), this.get('name')].join(' ');
  }.property('salutation', 'name')
});

Because we have specified that it depends on both salutation and name, changes to either of those two dependencies will invalidate the property, which will trigger an update to the {{author}} property in the DOM.

Imagine that in response to a user event, I do something like this:

1
2
post.set('salutation', "Mrs.");
post.set('name', "Katz");

You might imagine that these changes will cause the computed property to be invalidated twice, causing two updates to the DOM. And in fact, that is exactly what would happen when using an event-driven framework.

In Ember, the computed property will only recompute once, and the DOM will only update once.

How?

When you make a change to a property in Ember, it does not immediately propagate that change. Instead, it invalidates any dependent properties immediately, but queues the actual change to happen later.

Changing both the salutation and name properties invalidates the author property twice, but the queue is smart enough to coalesce those changes.

Once all of the event handlers for the current user event have finished, Ember flushes the queue, propagating the changes downward. In this case, that means that the invalidated author property will invalidate the {{author}} in the DOM, which will make a single request to recompute the information and update itself once.

This mechanism is fundamental to Ember. In Ember, you should always assume that the side-effects of a change you make will happen later. By making that assumption, you allow Ember to coalesce repetitions of the same side-effect into a single call.

In general, the goal of evented systems is to decouple the data manipulation from the side effects produced by listeners, so you shouldn't assume synchronous side effects even in a more event-focused system. The fact that side effects don't propagate immediately in Ember eliminates the temptation to cheat and accidentally couple code together that should be separate.

Side-Effect Callbacks

Since you can't rely on synchronous side-effects, you may be wondering how to make sure that certain actions happen at the right time.

For example, imagine that you have a view that contains a button, and you want to use jQuery UI to style the button. Since a view's append method, like everything else in Ember, defers its side-effects, how can you execute the jQuery UI code at the right time?

The answer is lifecycle callbacks.

1
2
3
4
5
6
7
8
9
10
11
12
App.Button = Ember.View.extend({
  tagName: 'button',
  template: Ember.Handlebars.compile("{{view.title}}"),

  didInsertElement: function() {
    this.$().button();
  }
});

var button = App.Button.create({
  title: "Hi jQuery UI!"
}).appendTo('#something');

In this case, as soon as the button actually appears in the DOM, Ember will trigger the didInsertElement callback, and you can do whatever work you want.

The lifecycle callbacks approach has several benefits, even if we didn't have to worry about deferred insertion.

First, relying on synchronous insertion means leaving it up to the caller of appendTo to trigger any behavior that needs to run immediately after appending. As your application grows, you may find that you create the same view in many places, and now need to worry about that concern everywhere.

The lifecycle callback eliminates the coupling between the code that instantiates the view and its post-append behavior. In general, we find that making it impossible to rely on synchronous side-effects leads to better design in general.

Second, because everything about the lifecycle of a view is inside the view itself, it is very easy for Ember to re-render parts of the DOM on-demand.

For example, if this button was inside of an {{#if}} block, and Ember needed to switch from the main branch to the else section, Ember can easily instantiate the view and call the lifecycle callbacks.

Because Ember forces you to define a fully-defined view, it can take control of creating and inserting views in appropriate situations.

This also means that all of the code for working with the DOM is in a few sanctioned parts of your application, so Ember has more freedom in the parts of the render process outside of these callbacks.

Observers

In some rare cases, you will want to perform certain behavior after a property's changes have propagated. As in the previous section, Ember provides a mechanism to hook into the property change notifications.

Let's go back to our salutation example.

1
2
3
4
5
App.PostController = Ember.ObjectController.extend({
  author: function() {
    return [this.get('salutation'), this.get('name')].join(' ');
  }.property('salutation', 'name')
});

If we want to be notified when the author changes, we can register an observer. Let's say that the view object wants to be notified:

1
2
3
4
5
6
7
8
App.PostView = Ember.View.extend({
  controller: null,
  template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>"),

  authorDidChange: function() {
    alert("New author name: " + this.get('controller.author'));
  }.observes('controller.author')
});

Ember triggers observers after it successfully propagates the change. In this case, that means that Ember will only call the authorDidChange callback once in response to each user event, even if both of salutation and name changed.

This gives you the benefits of executing code after the property has changed, without forcing all property changes to be synchronous. This basically means that if you need to do some manual work in response to a change in a computed property, you get the same coalescing benefits as Ember's binding system.

Finally, you can also register observers manually, outside of an object definition:

1
2
3
4
5
6
7
8
9
10
App.PostView = Ember.View.extend({
  controller: null,
  template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>"),

  didInsertElement: function() {
    this.addObserver('controller.author', function() {
      alert("New author name: " + this.get('controller.author'));
    });
  }
});

However, when you use the object definition syntax, Ember will automatically tear down the observers when the object is destroyed. For example, if an {{#if}} statement changes from truthy to falsy, Ember destroys all of the views defined inside the block. As part of that process, Ember also disconnects all bindings and inline observers.

If you define an observer manually, you need to make sure you remove it. In general, you will want to remove observers in the opposite callback to when you created it. In this case, you will want to remove the callback in willDestroyElement.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
App.PostView = Ember.View.extend({
  controller: null,
  template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>"),

  didInsertElement: function() {
    this.addObserver('controller.author', function() {
      alert("New author name: " + this.get('controller.author'));
    });
  },

  willDestroyElement: function() {
    this.removeObserver('controller.author');
  }
});

If you added the observer in the init method, you would want to tear it down in the willDestroy callback.

In general, you will very rarely want to register a manual observer in this way. Because of the memory management guarantees, we strongly recommend that you define your observers as part of the object definition if possible.

Routing

There's an entire page dedicated to managing async within the Ember Router: Asynchronous Routing