lordcasb

How to use dojo.Deferred, dojo.DeferredList, and .then

Discussion created by lordcasb on Jun 17, 2011
Latest reply on Jun 24, 2011 by danyim
I had a previous post explaining the old form of dojo.Deferred() that I think was useful.
I figured a post on Deferred in dojo 1.6 would be helpful too.

First, an example function. This function is a function on a larger widget. The idea of this function is that I have a property holding a map object (this.map), which may or may not be loaded, and a layer (the argument to the function) to add to the map, which may or may not be loaded.

I want this function to hold onto the layer until both the layer and map are loaded, and then add the layer to the map. This allows me to add operational layers of various projections immediately to the map, but have them held until after basemap layers of the projection I want have been added to the map.
addLayer: function (layer) {
 try {
  var dl = new dojo.Deferred();
  var dm = new dojo.Deferred();
  var dlOnLoad = dojo.connect(layer, "onLoad", dl, "callback");
  var dlOnError = dojo.connect(layer, "onError", dl, "errback");
  var dmOnLoad = dojo.connect(this.map, "onLoad", dm, "callback");
  dl.then(function(){dojo.disconnect(dlOnLoad);dojo.disconnect(dlOnError);});
  dm.then(function(){dojo.disconnect(dmOnLoad);});
  if (this.map.loaded) {
   dm.callback(this.map);
  }
  if (layer.loaded) {
   dl.callback(layer);
  }
  var df = new dojo.DeferredList([dm,dl]);
  return df.then(function(response){
   return (response[0][1] && response[1][1]) ? response[0][1].addLayer(response[1][1]) : false;
  });
 } catch (e) {
  console.error("Failed to add layer to map '"  + this.id + "'\n" + e);
  return;   
 }
}


Now, to step through this. First I create two new dojo.Deferred objects, dl (for the layer) and dm (for the map).
var dl = new dojo.Deferred();
var dm = new dojo.Deferred();


I connect the onLoad event of the layer to the callback function on the deferred and the onError event to the errback. layer.onLoad will return the layer, so that means that the layer itself will be the argument to the callback on dl. While I do not use the errback, connecting it makes things easier for anyone who extends my code.
var dlOnLoad = dojo.connect(layer, "onLoad", dl, "callback");
var dlOnError = dojo.connect(layer, "onError", dl, "errback");

Next, I connect the onLoad event for the map to the callback on dm. This is the same idea as the callback on dl. I am going to pass in a copy of the map (the result of the onLoad event) into the callback on dm. Maps do not have an onError event, so I have no call to errback for dm.
var dmonLoad = dojo.connect(this.map, "onLoad", dm, "callback");

Once I make these connects, as soon as the onLoad event fires, those arguments will be passed to the callback.
It is also good practice to disconnect your connects after they are no longer needed. I will explain .then() later.
dl.then(function(){dojo.disconnect(dlOnLoad);dojo.disconnect(dlOnError);});
dm.then(function(){dojo.disconnect(dmOnLoad);});



What if these events are already fired?
I check for that just in case by checking the .loaded properties on the map and layer. If they are loaded, then I pass the map and/or layer objects to the callbacks directly.
if (this.map.loaded) {
 dm.callback(this.map);
}
if (layer.loaded) {
 dl.callback(layer);
}

What matters here is that the argument being passed to the callback function will be passed to all the registered callbacks on dl and dm respectively.


Now, before I get to the next step, I need to cover the promise.  Each of these Deferred objects has a .promise property which stores a promise object. To understand the promise more in depth, look here:
http://dojotoolkit.org/documentation/tutorials/1.6/promises/
Basically though, a promise represents an eventual value. In this case, dl's promise is going to eventually be the loaded layer object. dm's promise is eventually going to be the loaded map object. When the onLoad events pass the loaded map or layer objects, or when I directly pass the already loaded map or layer objects, that promise is fulfilled. (The errback represents that the promise is rejected and cannot be fulfilled.)

Promises can be chained. When the promise is fulfilled, it passes it values to other promises changed off of it. The original promise value remains unchanged no matter what happens to the value after it passes to chained promises. That is an important concept to remember. When you add a callback function, you are chaining a new promise onto the original promise. When you call .callback() on a promise, you are fulfilling the promise with a value.

So, calling dm.callback(this.map) or dl.callback(layer) passes that value to the chained promises on dm.promise or dl.promise.

But, we need to make sure that -both- the map and the layer are loaded. Enter dojo.DeferredList.
DeferredList lets you deal with the situation where you are looking for multiple promises to be fulfilled, one promise out of several to be fulfilled, or even for one of several promises to be rejected. In this case, we are waiting for both dm.promise and dl.promise to be fulfilled.

So, we create a deferred list called df that will wait for the promises to be fulfilled from both dm and dl.
var df = new dojo.DeferredList([dm,dl]);

df, in turn, has its own promise, df.promise, attached to it. This promise will be fulfilled when both promises in the list, dm and dl, are fulfilled. So what value is returned if it is taking multiple values? All of them. So, I want to want until the promise on df is fulfilled, and then execute a new function using the results that will add the layer to the map.

So that, brings us to the last line:
return df.then(function(response){
 return (response[0][1] && response[1][1]) ? response[0][1].addLayer(response[1][1]) : false;
});

Start with df.then

.then() is a special function on a promise. Remember when I talked about chaining a promise on a promise? The .then() function creates that chained promise. .then() returns a new promise that will be fulfilled when it receives the value from the original promise, df in this case. The reason for this, again, is that this keeps the original promise value unmodified. I can call df.then() multiple times, chaining multiple promises off df and each of them will receive a copy the exact same set of values no matter what the other chained promises do with their copy. Realize though that if I call df.then().then(), that the second .then will receive its promised values from the first .then(), not from df, after the first .then() has modified the values it receives from df.

The argument for .then() is a function, which will receive the values from our original promise to use as arguments. Since our original promise is our deferred, df, the format of those arguments will be an array showing whether or not each of the promises in the DeferredList was successfully delivered, and the value delivered.
In this case, if the map and layer both successfully load, that will be:
[[true, <map object>],[true,<layer object>]]

That means that response[0][1] is our loaded map object and response [1][1] is our loaded layer object.
So, when I call
return (response[0][1] && response[1][1]) ? response[0][1].addLayer(response[1][1]) : false;

I am really saying: "if the map object exists in the response and the layer object exists in the response, then call map.addLayer(layer) and return the result (which should be the layer again), otherwise, return false)"

This call uses ternary notation. I could also have written
if (response[0][1] && response[1][1]) {
 return response[0][1].addLayer(response[1][1]);
} else {
 return false;
}


So, that executions the function I want, map.addLayer, and then passes back the layer after is has been added to the map (or false if this fails).

Notice though that instead of just calling "df.then(<function>)" I actually do "return df.then(<function>)". Why?

When I call my original .addLayer() function, I have no guarantee that it can be executed right away. I might be waiting a while for the map or layer to load. But, I want to eventually return the layer after it is added to the map. The solution is to return another promise!

Remember that df.then() creates a new promise that is chained to df. That new promise is returned by the .then() function call. That promise should be fulfilled with the layer after it has been added to the map, exactly what I want to return.

So, I have .addLayer return the promise created by df.then(). Now, if someone needs to execute operations on that layer after it is added to the map, they can take the promise return from my version of the .addLayer function and chain their functions off of that!

Best of all, if when they go to execute their function the map and layer are already loaded and the loaded layer is already available, the promise is already fulfilled and their function will execute right away. No more checking to make sure the map and layer are both already loaded and that the layer has been added to the map!

This is just one small example of how to use dojo.Deferred, dojo.DeferredList and .then().

There is also the powerful function dojo.when(), which can be used when you do not know if a variable will be a promise or a value when execute. In that situation, .when will promise either the variable's value or the fulfilled value of the promise that the variable represents. As I mentioned too, DeferredList can represent multiple conditions besides all values being successfully returned too.
See these tutorials for more help
Deferred and DeferredList
http://dojotoolkit.org/documentation/tutorials/1.6/deferreds/
Promises, then, and when
http://dojotoolkit.org/documentation/tutorials/1.6/promises/

Hopefully this has been informative, and I have everything correct. Feel free to post any corrections and requests for additional information on this topic.

Outcomes