Image Preloader using JQuery
I recently was creating an image viewer for a customer. It had the usual qualities of an image viewer where you need preload images to improve response times for the user.
Requirements
- Load X images behind/infront of current image
- Handle image load errors
- Callback for load complete
- Tie attributes to an image
In addition, we will use JQuery to facilitate this.
Lets Get Started
So if we abstract this a little, we want to have an Image Object with the following attributes…
ImageObject
- url – The url of the image to load
- callback – A callback function for when the image load is complete
- scope – With javascript, closure, we want to call the callback within a scope other than window. This will hold this information.
- loaded – Boolean so we can track the images state.
- loadFailed – So with an image, we may have failed to load or timeout. So we need to track that.
- timeout - How long to wait on things to load.
/**
* @classDescription Image object which holds information about the image which is preloading.
* This object can be prototyped with extendImage.
* @param {String} url The url of image
* @param {Function} callback The users callback function
* @param {Object} scope The scope to call callback function in
* @param {Boolean} loaded Whether the image has been loaded
* @param {Jquery Object} $img The reference to the jquery image object in preloading node.
* @param {Object} objRef This object's reference
* @see extendImage()
*/
_Image : function(url, callback, scope, loaded, $img, objRef){
this.url = url;
this.callback = callback;
this.scope = scope;
this.loaded = loaded;
this.$img = $img;
this.func = objRef._imageLoadComplete;
this.loadFailed = true;
this.timeout = 5000;//5 seconds
this.start = new Date().getTime();
this.finish = 0;
}
Now We Need to Store the Image Objects
So if we are going to have an image object to represent each image, then we need a way to store these and be able to get them. Unfortunately javascript doesn’t by default have a good way for use to store and retrieve data. The default array, is somewhat limited. Fortunately we can create indexes of the array with strings. Basically giving us a simple hash table.
However, I kinda wanted to spice this up a little. I could have used the URL of the image as the index, but what fun is that. So lets create a hash of the URL and use that as an index.
hashCode = function(str){
str = str + "";
var hash = 0;
if (str.length == 0) return hash;
var len = str.length;
for (var i = 0; i < len; i=i+1) {
char = str.charCodeAt(i);
hash = ((hash<<5)-hash)+char;
hash = hash & hash; // Convert to 32bit integer
}
return hash >>> 1; //I want positive numbers
}
Now you will probably argue that looping over each character is bad, why not just use the URL. Well, your probably right, but this is more fun.
hash = ((hash<<5)-hash)+char;
This is better explained here.
Initiate the Pre-loading of an Image
We will need a simple function to load images. Everything that refers to “this” refers to the object that wraps all these functions.
preload = function(url, callback, scope, timeout){
var hash = this._hashCode(url);
if(this.isLoaded(hash)) return;
if(typeof scope === "undefined") scope = window;
//create image object
var $img = $(document.createElement('img'))
.attr('src',url)
.attr('id',hash);
this._images[hash] = new this._Image(url, callback, scope, false, $img, this);
//bind the onload...pass vars using closure
this._images[hash].$img.load(function(obj, hash){
//only executes on successful load
return function(){obj._images[hash].func.call(obj, obj._images[hash]);};
}(this, hash));
//we always want to trigger our functions, but jquery only
//callsback on success, so have to keep polling at the image timeout
if(typeof timeout !== "undefined") this._images[hash].timeout = timeout;
this.waitOnImage(url, this._imageLoadComplete, this, this._images[hash].timeout, 2);
this._$loadNode.append(this._images[hash].$img); //append loaded image to the loading DOM element
return this._images[hash];
};
There are actually a couple interesting segments of this code.
scope - This will allow us to scope the context to the callback. This is useful to avoid namespace collisions and ensure you are triggering the correct event to specified image loading.
this._images[hash].$img.load(function(obj, hash){
//only executes on successful load
return function(){obj._images[hash].func.call(obj, obj._images[hash]);};
}(this, hash));
This uses the JQuery default load callback. The problem with the load event is it is unreliable. It won’t fire if the image fails to load and won’t fire reliably cross-browser and in other circumstances. These are describer in the jquery link.
Handle the load event not working
To do this we basically need some sort of polling to check if the image is loaded. That is the waitOnImage() function.
WaitOnImage Function
/**
* Bind specific callback to wait on image
* @param {String} url The url of image to wait on load for
* @param {Function} callback Callback function to call when loaded
* @param {Object} scope The scope to call callback in. Default to window.
* @param {Integer} perdiod Time in milliseconds between recalling wait
* @param {Integer} maxTries max attempts to call this function
* @param {Integer} tryCount defaults to 0. Used for cancelling this.
*/
waitOnImage : function(url, callback, scope, period, maxTries, tryCount){
if(typeof period === "undefined") period = 50;
if(typeof maxTries === "undefined") maxTries = 100;
if(typeof scope === "undefined") scope = window;
if(typeof tryCount == "undefined") tryCount = 0;
var hash = this._hashCode(url);
var imgObj = this._images[hash];
if(typeof imgObj === "undefined"){
this.preload(url, null, null);
} else if(imgObj.loaded || tryCount > maxTries){
callback.call(scope, imgObj);
return;
}
//use closures to call self
setTimeout(
function(url, callback, scope, period, maxTries, tryCount, _this){
return function(){
_this.waitOnImage(url, callback, scope, period, maxTries, tryCount);
};
}(url, callback, scope, period, maxTries, ++tryCount, this)
,period);
}
The interesting part of the code here is the setTimeout function call. This accomplishes 2 things.
- Keeps this from blocking. Meaning, setTimeout will execute in parallel to this code blocks execution. If you look at the preload function you see it call this function, which if it just continuously called itself, it would block and prevent the function from completing until the image finished loading. This would be bad as images can sometimes take quite a while to load.
- allows for closure to scope the variables to this execution.
Extensibility?
So we have covered the basics of creating a preloader, but its so basic we really need a mechanism to extend its functionality.
/**
* Access the prototype to _Image to add your own attributes.
* @param {String} name Name of the reference in prototype.
* @param {Object} obj The function that will be referenced by name.
*/
extendImage : function(name, func){
//todo add prototype stuff
this._Image.prototype[name] = func;
}
This alows us to add whatever we want to the _Image object. Its really just for accessibility, but a nice feature none the less. Check out the source code and documentation here.
Recent Comments