I wanted a gallery to display photos, so wrote a quick application in native Javascript to do it. I’m quite please with the result, although so far it is basic. CSS sizing and positioning mean that it works well on mobile and honours screen rotation, all for free, which is very nice for showing the images. I can just rotate the screen depending on the image orientation.
I wanted the Promises framework I had written two or three years ago for a previous employer. So I re-implemented it, perhaps getting to bed a little late last night. I’m so glad I did! Promises is a standard in Javascript, though my implementation has minor differences. The second attempt benefits from hindsight. Features that were unused in the original were not implemented, so simplifying the system. The original grew in an agile fashion so has ways of solving things that became redundant in later iterations as better ways evolved. The code size for the new version is quite smaller than the original. A job for my work week would be to port it to Ext-JS for work to use at work.
The Promises code is currently a Git-Hub Gist. When it gains unit tests I’ll likely have to make it a full repository. So what does it give me? In short:
- Easy coding of asynchronous workflow avoiding the mess of callback chaining.
- Componentised workflows, a block can seamlessly encapsulate its own workflow.
- Ability to split and join asynchronous workflows – try that with Ext’s callback mechanism!
- Convenient error handling in asynchronous workflows.
- Simpler, shorter, more readable code
So how is it used in Gallery?
Gallery has an Image Preloader which uses a pool of invisible image tags to bring images into the browser cache. It loads two ahead and one behind the currently displayed image so allowing faster response when the user navigates. If the user navigates too fast then a spinner is displayed for that image. The user can still navigate, and will see the spinner change “Loading picture of cat” or the actual picture of the cat depending on whether the image is loaded. The system uses Promises to track the loading of each image.
Here is the loadImage static method of ImagePreloader. The original loader.loadImage method uses callbacks. I didn’t see need to change this. The static method returns a Promise of the image being loaded.
ImagePreload.loadImage = function (image) { var promise = new Promise(); if(!this.loaderPool.some(function (loader) { if(!loader.busy) { loader.loadImage(image, promise.resolve, promise); return true; } })) { this.loaderPool.push(new ImagePreload(image, promise.resolve, promise)); } return promise; };
The Image model class (ImageData) can provide its own loadImage() function which returns a Promise of the image having been loaded. If it already has a Promise, because it is loading or it has loaded, then that is returned. Otherwise it creates a new Promise and starts the load. The use of the “then” method takes the result of one Promise and transforms it, returning a new Promise of the transformed result. The transformed result in this case is the ImageData itself, so that the recipient can know which image completed. The second argument to promise.then is a scope argument, so causing “return this;” to have the right value of “this”.
/** * Load the image if needed. * @returns {Promise} promise of the load, result is this ImageData */ loadImage : function () { if(!this._loadPromise) { this._loadPromise = ImagePreload.loadImage(this.url) .then(function () { return this; }, this); } return this._loadPromise; }
Gallery’s setImage method uses the loadImage method of the ImageData model. When this is complete (or immediately if already complete) it uses Gallery’s onImageReady to switch the images. Note that the result of this method is a Promise. When handling a Promise’s result you can return a Promise of future result.
/** * @private * @param {Number} index image index to display * @returns {Promise} promise of the image having been displayed. */ setImage : function (index) { var imageData = this.data[index]; this.currentIndex = index; if(!imageData.isImageLoaded()) { this.ajaxSpinner.show('Loading image ' + imageData.name, true); } this.triggerNeighbouringImagePreload(); return imageData.loadImage() .then(this.onImageReady, this); }
The onImageReady method needs to check that the image is the one it expects, because the user may have set a few of these off before the first one can return.
/** * @private * Handle image ready * @param {ImageData} imageData the image data that is reporting the load complete. */ onImageReady : function (imageData) { var display; if(imageData === this.data[this.currentIndex]) { display = (this.currentIndex + 1) + ' of ' + (this.data.length) + ': ' + imageData.name; this.ajaxSpinner.show(display, false); this.photoImage.src = this.data[this.currentIndex].url; this.photoImage.style.display = "block"; } },
When the application starts up it loads control data and shows the first image. If there’s an error then this is displayed. This is where Promises start to become powerful, as these steps all have their own internal processes, they may complete synchronously or asynchronously and an error in any step propagates down.
loadJson(me.dataUrl) .then(me.parseImageData, me) .then(function(){ me.setImage(0); }) .otherwise(me.onDataError, me);
I don’t like throwing exceptions except in exceptional situations. A promise handler can throw an exception to return an error, or it can return a rejected Promise. A convenience method was created to allow this. Here’s the parseImageData method:
/** * @private * Read and store the server data. * @param {String} data JSON data from the server * @return {Promise|undefined} */ parseImageData: function (data) { this.data = JSON.parse(data) .map(function initData(d) { return new ImageData(d); }); if(this.data.length == 0){ return Promise.reject('No images specified in image data.'); } }
On refactoring
The preloading does not always seem effective on my mobile phone browser. I don’t know if it’s not caching, not loading invisible images, or taking too long to render the image it has loaded. One thing I could do is extract the photo display to its own component. This would abstract away things like transitions if I ever implement them. Its setImage() method would return a Promise of the image being successfully set. Unlike the preloader, this promise could be rejected if the user changes image before the promised image has had chance to load. It would be easy to return the new promise from onImageReady, or add a new displayImage() step into the chain, so that the spinner doesn’t stop spinning until all is completed. This kind of flexibility is not so easy to achieve in a callback based system.
What is there to do?
- Complete Promise Joining. I didn’t need it for Gallery, but it’s a simple thing to do.
- Unit Tests and proper packaging, including a minified version.
- Work out a standard mechanism for passing errors so the end recipient can better deal with them. In Gallery I pass a String message which is fine for that application, but bigger apps may want more. Errors in Promises are like Exceptions in that they can be thrown some way if you have a long complex workflow.
A nice pattern for errors would be to support an error transform method, which would allow implementation of the try-catch-throw pattern in other languages. So a Java loader may catch IOException and throw some kind of MyLoaderException to hide its internals, a Promise error transformer could achieve the same. Promises already provide a thenAlways() method to ensure that execution continues regardless of state.