link Further Deferreds examples
Deferreds are used behind the hood in Ajax but it doesn't mean they can't also be used elsewhere. This section describes situations where deferreds will help abstract away asynchronous behavior and decouple our code.
link Caching
link Asynchronous cache
When it comes to asynchronous tasks, caching can be a bit demanding since you have to make sure a task is only performed once for a given key. As a consequence, the code has to somehow keep track of inbound tasks.
1
2
|
|
The caching mechanism has to make sure the URL is only requested once even if the script isn't in cache yet. This shows some logic to keep track of callbacks bound to a given URL in order for the cache system to properly handle both complete and inbound requests.
1
2
3
4
5
6
7
8
9
10
|
|
One promise is cached per URL. If there is no promise for the given URL yet, then a deferred is created and the request is issued. When the request is complete, the deferred is resolved (with defer.resolve
); if an error occurs, the deferred is rejected (with defer.reject
). If the promise already exists, the callback is attached to the existing deferred; otherwise, the promise is first created and then the callback is attached. The big advantage of this solution is that it will handle both complete and inbound requests transparently. Another advantage is that a deferred-based cache will deal with failure gracefully. The promise will end up rejected which can be tested for by providing an error callback:
1
|
|
link Generic asynchronous cache
It is also possible to make the code completely generic and build a cache factory that will abstract out the actual task to be performed when a key isn't in the cache yet:
1
2
3
4
5
6
7
8
9
10
11
|
|
Now that the request logic is abstracted away, $.cachedGetScript()
can be rewritten as follows:
1
2
3
|
|
This will work because every call to $.createCache()
will create a new cache repository and return a new cache-retrieval function.
link Image loading
A cache can be used to ensure that the same image is not loaded multiple times.
1
2
3
4
5
6
7
8
9
10
11
12
|
|
Again, the following snippet:
1
2
|
|
will work regardless of whether my-image.png
has already been loaded or not, or if it is actually in the process of being loaded.
link Caching Data API responses
API requests that are considered immutable during the lifetime of your page are also perfect candidates. For instance, the following:
1
2
3
4
5
6
7
8
9
10
11
|
|
will allow you to perform searches on Twitter and cache them at the same time:
1
2
|
|
link Timing
This deferred-based cache is not limited to network requests; it can also be used for timing purposes.
For instance, you may need to perform an action on the page after a given amount of time so as to attract the user's attention to a specific feature they may not be aware of or deal with a timeout (for a quiz question for instance). While setTimeout()
is good for most use-cases it doesn't handle the situation when the timer is asked for later, even after it has theoretically expired. We can handle that with the following caching system:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
|
The new $.afterDOMReady()
helper method provides proper timing after the DOM is ready while ensuring the bare minimum of timers will be used. If the delay is already expired, any callback will be called right away.
link One-time event
While jQuery offers all the event binding one may need, it can become a bit cumbersome to handle events that are only supposed to be dealt with once.
For instance, you may wish to have a button that will open a panel the first time it is clicked and leave it open afterward or take special initialization actions the first time said button is clicked. When dealing with such a situation, one usually ends up with code like this:
1
2
3
4
5
6
7
8
9
|
|
then, later on, you may wish to take actions, but only if the panel is opened:
1
2
3
4
5
|
|
This is a very coupled solution. If you want to add some other action, you have to edit the bind code or just duplicate it all. If you don't, your only option is to test for buttonClicked
and you may lose that new action because the buttonClicked
variable may be false
and your new code may never be executed.
We can do much better using deferreds (for simplification sake, the following code will only work for a single element and a single event type, but it can be easily generalized for full-fledged collections with multiple event types):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
|
The code works as follows:
- Check if the element already has a deferred attached for the given event
- if not, create it and make it so it is resolved when the event is fired the first time around
- then attach the given callback to the deferred and return the promise
While the code is definitely more verbose, it makes dealing with the problem at hand much simpler in a compartmentalized and decoupled way. But let's define a helper method first:
1
2
3
|
|
Then the logic can be re-factored as follows:
1
2
3
4
|
|
If an action should be performed only when a panel is opened later on:
1
2
3
4
5
|
|
Nothing is lost if the panel isn't opened yet, the action will just get deferred until the button is clicked.
link Combining helpers
All of the samples above can seem a bit limited when looked at separately. However, the true power of promises comes into play when you mix them together.
link Requesting panel content on first click and opening said panel
Following is the code for a button that, when clicked, opens a panel. It requests its content over the wire and then fades the content in. Using the helpers defined earlier, it could be defined as:
1
2
3
4
5
6
7
8
9
|
|
link Loading images in a panel on first click and opening said panel
Another possible goal is to have the panel fade in, only after the button has been clicked and after all of the images have been loaded.
The HTML code for this would look something like:
1
2
3
4
5
6
|
|
We use the data-src
attribute to keep track of the real image location. The code to handle our use case using our promise helpers is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
|
The trick here is to keep track of all the $.loadImage()
promises. We later join them with the panel .slideDown()
animation using $.when()
. So when the button is first clicked, the panel will slide down and the images will start loading. Once the panel has finished sliding down and all the images have been loaded, then, and only then, will the panel fade in.
link Loading images on the page after a specific delay
In order to implement deferred image display on the entire page, the following format in HTML can be used.
1
2
3
4
|
|
What it says is pretty straight-forward:
- Load
image1.png
and show it immediately for the third image and after one second for the first one - Load
image2.png
and show it after one second for the second image and after two seconds for the fourth image
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
|
In order to delay the loading of the images themselves:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
|
Here, after the delay to be fulfilled then the image is loaded. It can make a lot of sense when you want to limit the number or network requests on page load.