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

9353
7
06-17-2011 10:12 AM
BrettLord-Castillo
Occasional Contributor
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.
7 Replies
derekswingley1
Frequent Contributor
Brett,

Fantastic post! Thank you for taking the time to write and post this. I think you nailed it and this post will no doubt help those trying to get their head around how deferreds and promises work.

The only small correction, and it's a tiny one, would be to change "trinary" to "ternary" as I think the latter is the more readily used name for that operator.

Also, I think your old post/thread on dojo.Deferred is still available:  http://forums.esri.com/Thread.asp?c=158&f=2396&t=295896&mc=6#msgid924290

Again, thanks for this post!
0 Kudos
BrettLord-Castillo
Occasional Contributor
Took care of the edit, and tweaked the example to include disconnects.
In practice, without disconnects the function failed when passing it feature classes.
0 Kudos
DanielYim
New Contributor II
Wonderful post! I have been attempting to wrap my head around Dojo.Deferred for a while now, and your post here and your posts that I've read via forum search have helped significantly.

Some questions:

Could you mention some practical scenarios where using/handling a Dojo.Deferred is preferred over the typical callback and errback parameters found in many of the API's asynchronous functions?

Can you provide more practical examples or scenarios where one would use Dojo.Deferred? For instance, your post is the scenario where we need to add a layer after the map and another layer has been successfully loaded.

Could you please clarify the response[0][1]? I am not sure what these represent.

Thanks
0 Kudos
BrettLord-Castillo
Occasional Contributor
When you are using the callback and errback arguments, you are using a Deferred and most of the time that is all you will do with it.
But as you do more complex data handling, you will run into more complex uses of Deferred and Deferred List; situations where you need multiple ajax operations to finish before proceeding.
Also, if you use concepts like lazy loading, where you might wait on one load operation to finish before proceeding or call multiple loads in the background, dojo.Deferred can get very useful.

You will still mostly run into this when doing tasks, where you will see that your return object is normally a dojo.deferred. The callback and errback arguments can work fine on this, but it is good to understand what is going on behind those parameter calls.
0 Kudos
DanielYim
New Contributor II
Thanks for the clarification.

I am not sure if this is related or not, but suppose I want to run a function only once after a certain event is finished.

Let's say that the event is the map's onUpdateEnd, so after the map is finished zooming/panning, I want it to call some anonymous function as such:
var tempHook;
tempHook = dojo.connect(myMap, "onUpdateEnd", function() {
    // do things here
    
    // Disconnect so this doesn't run again in the next onUpdateEnd
    dojo.disconnect(tempHook);
    tempHook = null;
};


How can I incorporate Dojo.deferred to improve this? Currently the above method is what I have been doing to sync my code.
0 Kudos
BrettLord-Castillo
Occasional Contributor
I'm not sure you want to use dojo.Deferred in that situation.
You are pretty much doing things the correct way by setting up a dojo.connect and disconnecting that connect once it is fired.
You could use the pattern I used, where you connect to the deferred and call "callback" on the deferred, then chain your function you want run onto the deferred. But that seems unnecessary in this situation.
Are you trying to synchronize calls between onUpdateStart and onUpdateEnd?
Or trying to time something with the first time a map updates?
0 Kudos
DanielYim
New Contributor II
Well, a specific example of how I use the aforementioned structure is when I call the map's centerAndZoom() function to center and zoom in on a point and then open an InfoWindow. Basically, the event I am listening for is "wait for the map to stop moving," because the InfoWindow will be anchored incorrectly if I invoke it during a map update, as opposed to after.

In my experience, this kind of forced synchronization is required very often or else things just don't work as they should. I am getting a feeling that dojo.Deferred can help me out in this regard...but I am not exactly sure how.
0 Kudos