Browse DevX
Sign up for e-mail newsletters from DevX


Develop a Reusable Image Cache in JavaScript : Page 2

The asynchronous download ability built into today's browser DOMs is a great way of getting image-heavy pages to build faster, but it doesn't mesh well with JavaScript's single-threading execution model. Use our image cache API to build scripts that work hand-in-hand with asynchronous objects and their events.




Building the Right Environment to Support AI, Machine Learning and Deep Learning

Image Caching and Image Groups
To make things easier, I decided to develop a more general framework for handling images. In doing so, I wanted to encompass more than just "preloading" them (a vague term, at best). Instead, I tried to decide what users commonly want to do with images and then develop an API that would satisfy those needs, while making it as easy as possible to work around the fact that images don't load synchronously. My conclusion was that—so far as synchronized loading goes—images belong in groups: navigation groups, icon groups, content groups, rollover groups. Sometimes the groups that interest us are even larger—for example, the set of images on the current page or on the Web site as a whole—but all that really concerns us is that the group of images in question has or has not completed loading. We then use this information to scale related graphics to common sizes, align thumbnails, relatively displace document elements, or simply load and display images together as a set.
All you have to do is put any code you want to execute after your set of images has loaded into a separate method, and then specify this method as the group's completion handler.

The initial API I came up with was a single, self-contained script that used a few globals to control the interactions between two collaborative objects. Here's how it works. A top level ImageCache holds all the images you've ever tried to load since it was instantiated, and you interact with it by creating an ImageGroup, adding the set of images you want to load to it, and then asking the cache to go off and get them. The cache then checks if any of the requested images have already been loaded, and, if not, starts to load them. Finally, when all the images are available, it executes a callback of your choice so that you can do something with the set as a whole.

What's good about it is it's generic; you use it the same way whether you want to load one image or 100. The callback may seem a bit of a kludge, but it's the only sensible way to get around the threading issues we've just covered. It also means it's very easy to adapt your code to achieve the semblance of synchronicity you wanted all along. All you have to do is put any code you want to execute after your set of images has loaded into a separate method, and then specify this method as the group's completion handler.

For example, to return to the slideshow scenario I discussed earlier, here's how to code LoadImages() so that DoSomethingElse() only runs when all the images have finished loading:

function LoadImages() { // Create an ImageGroup and add any images to load var group = new ImageGroup(); group.AddImage("_images/A1.jpg"); group.AddImage("_images/A2.jpg"); ... // Set a completion handler group.OnComplete = "javascript:DoSomethingElse()"; // Load the group g_image_cache.LoadGroup(group); }

Strictly speaking, you don't need an image cache if all you want is to be notified when a set of images has loaded. Without it, though, any code that tries to load an image that has already been loaded by another piece of code would end up creating a separate image resource each time, which is wasteful. It would then go ahead and load the image all over again, which would be even sillier!

As it is, the ImageGroup and ImageCache objects have an efficient relationship: The ImageCache ensures no image is loaded more than once, and ImageGroup objects make sure to return cached images if they are available. Consequently, after the initial loading's been done, successive calls to the LoadImages() method above simply result in DoSomethingElse() getting called straight away.

Providing Feedback
Though we've simplified the job of getting a particular asynchronous task to interact with synchronous ones in a consistent way, the fact remains that the "old" way has its advantages. Most importantly, you get constant feedback on the loading process, meaning that, while you might have to wait a while for large images to download, at least you can see that they're progressing.

Because JavaScript can't raise its own custom events (unless it's a scriptlet), there are two obvious ways you can mimic this behaviour using my API as its stands:

  • Have the loading process proactively "push" its state of completion into the document, in the form of well-defined properties; or
  • Images fire onreadystatechange events as they load. Handle these events and try to relate the information back to the progress of some particular set of images you asked to load
The first solution isn't the most elegant, but in most practical cases it's probably the easiest way to get the effect you want. Suppose for example the API was bound to some document element called progressElement, which exposed a property called width. Without further ado you could get instantaneous graphical feedback on the group you were loading simply by dropping in a bit of HTML code like this:

<img src="loader-line.jpg" id="progressElement" width=0> or <td bgcolor="red" id="progressElement" width=0>

As the images loaded, document.progressElement.width would get updated, causing the loader line to widen accordingly. Ultimately, though, I had to reject this solution on account of its inflexibility. If the loader line's width was specified in pixels, you would need some way to tell the API the maximum width it should return, in order to accommodate different length "progress bars."

But maybe you wouldn't want a progress bar after all; maybe you would prefer to display the percentage complete as a string? Or perhaps you don't want to display anything at all; you just want to know how many images in the current group have loaded, and how many are left to load? Each case would require the API to expose a separate document property—a classic example of the potential pitfalls you can expect when you tightly bind controls without sufficient forethought.

The second solution also has its drawbacks. We would have to be careful only to catch state change events that originated from images preloaded by the image cache, so we didn't interfere with any other asynchronous elements. At the same time, for reasons of data abstraction, it would be nicer not to have to poke around with global variables to find out the information we want.

In fact, we can get both of these effects, and even something approaching the "custom event" we wanted all along, by adding an additional layer of abstraction:

  • Hook the readystatechange event for each image loaded via an ImageGroup object
  • Each time the hook gets invoked, populate some kind of "progress" object with appropriate data, and pass it as a parameter to a user-defined update handler (if specified). Because an ImageGroup already knows which images need to be loaded and how many have loaded to date, it makes sense simply to pass this as our "progress" object.
This way, you effectively adapt (i.e. wrap) an in-built document event (readystatechange) to create the illusion of a custom event (imageupdate) with its own originator (an ImageProgress object), which you can hook up in exactly the same way as any other document event. Assuming you're interested in the event, you assign it to a handler of your choice, within which you can script the document in any way you see fit.

This is a much better model than the initial one discussed in this article, and since you can now establish loading progress within the imageupdate handler, ImageGroup no longer needs its OnComplete() delegate; you can just call its IsComplete() method to see when it's finished.

To get maximum usefulness out of the API though you also need to provide a way to retrieve images directly from the cache, so that scripts with no direct involvement in loading images themselves can search for them and manipulate them if they exist. The GetImage() method, exposed on the ImageCache object, does just this. You'll see it used in the setImages() method of auto-load.js to iterate through the set of images on the page and display them all together once they've finished loading.

Finally, in order to dissuade you from working directly with g_image_cache, the global that holds an instance of ImageCache, I've wrapped and re-exposed the cache's two salient methods, LoadGroup() and GetImage(), at the top level. This doesn't mean you can't manipulate the cache directly if you want, but hopefully it'll encourage you to think twice before doing so frivolously. Listing 1 presents the final code for load-image.js.

Comment and Contribute






(Maximum characters: 1200). You have 1200 characters left.



Thanks for your registration, follow us on our social networks to keep up-to-date