Waiting until resources are loaded
In the Load resources asynchronously recipe, we showed how you can load external Three.js resources asynchronously. For many sites and visualization, loading resources asynchronously is a good approach. Sometimes, however, you want to make sure that all the resources you require in your scene have been loaded beforehand. For instance, when you're creating a game, you might want to load all the data for a specific level beforehand. A common method of loading resources synchronously is nesting the asynchronous callbacks we've seen in the previous recipe. This, however, quickly becomes unreadable and very hard to manage. In this recipe, we'll use a different approach and work with a JavaScript library called Q.
Getting ready
As for all the external libraries that we use, we need to include the Q library in our HTML. You can download the latest version of this library from its GitHub repository at https://github.com/kriskowal/q, or use the version provided in the libs
folder in the sources for this book. To include this library in your HTML page, add the following in the head
element of your HTML page:
<script src="../libs/q.js"></script>
In the sources for this chapter, you can also find an example where we load resources synchronously. Open 01.12-wait-for-resources.html
in your browser and open the JavaScript console:
On the console output, you'll see that the required resources and models are loaded one after another.
How to do it...
- Let's first take a look at what we're aiming for in this recipe. We want to load resources synchronously, using the Q library, in the following manner:
loadModel(model) .then(function(result) {return loadTexture(texture)}) .then(function(result) {return loadModel(m)}) .then(function(result) {return loadTexture(texture)}) .then(function(result) {return loadOthers(resource)}) .then(function(result) {return loadModelWithProgress(m)}) .then(function(result) {return loadModel(model)}) .then(function(result) {return loadOthers(resource)}) .then(function(result) {return loadModel(model)}) .then(function() {console.log("All done with sequence")}) .catch(function(error) { console.log("Error occurred in sequence:",error); }) .progress(function(e){ console.log("Progress event received:", e); });
- What this code fragment means is that:
- Firstly, we want to call
loadModel(model)
. - Once the model is loaded, we load, using the
then
function, a texture using theloadTexture(texture)
function. Once this texture is loaded, we will then load the next resource and so on. In this code fragment, you can also see that we also call acatch
and aprogress
function. If an error occurs during loading, the function provided tocatch()
will be called. The same goes forprogress()
. If one of the methods wants to provide information about its progress, the function passed intoprogress()
will be called. - However, you will then find out that this won't work with the functions from our previous recipe. To get this to work, we have to replace the callbacks from these functions with a special Q construct that is called a deferred function:
function loadTexture(texture) { var deferred = Q.defer(); var text = THREE.ImageUtils.loadTexture (texture, null, function(loaded) { console.log("Loaded texture: ", texture); deferred.resolve(loaded); }, function(error) { deferred.reject(error); }); return deferred.promise; }
- In this code snippet, we create a new JavaScript object with the name
deferred
. Thedeferred
object will make sure that the results of the callbacks, this time defined as anonymous functions, are returned in such a way that we can use thethen
function we saw at the beginning of this chapter. If the resource is loaded successfully, we use thedeferred.resolve
function to store the result; if the resource was loaded unsuccessfully, we store the error using thedeferred.reject
function. - We use the same approach for the
loadModel
,loadOthers
, andloadModelWithProgress
functions:function loadModel(model) { var deferred = Q.defer(); var jsonLoader = new THREE.JSONLoader(); jsonLoader.load(model, function(loaded) { console.log("Loaded model: ", model); deferred.resolve(loaded); }, null); return deferred.promise; } function loadOthers(res) { var deferred = Q.defer(); var xhrLoader = new THREE.XHRLoader(); xhrLoader.load(res, function(loaded) { console.log("Loaded other: ", res); deferred.resolve(loaded); }, function(progress) { deferred.notify(progress); }, function(error) { deferred.reject(error); }); return deferred.promise; }
- In the
loadOthers
function, we are also provided with the progress information. To make sure that the progress callback is handled correctly, we use thedeferred.notify()
function and pass in theprogress
object:function loadModelWithProgress(model) { var deferred = Q.defer(); var jsonLoader = new THREE.JSONLoader(); jsonLoader.loadAjaxJSON(jsonLoader, model, function(model) { console.log("Loaded model with progress: ", model); deferred.resolve(model) }, null, function(progress) { deferred.notify(progress) }); return deferred.promise; }
- With these changes, we can now load the resources synchronously.
- Firstly, we want to call
How it works...
To understand how this works, you have to understand what Q does. Q is a promises library. With promises, you can replace the nested callbacks (also called the Pyramid of doom at http://calculist.org/blog/2011/12/14/why-coroutines-wont-work-on-the-web/) with simple steps. The following example for the Q site nicely shows what this accomplishes:
step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // Do something with value4 }); }); }); });
Using promises, we can flatten this to the following (just like we did in this recipe):
Q.fcall(promisedStep1) then(promisedStep2) then(promisedStep3) then(promisedStep4) then(function (value4) { // Do something with value4 }) catch(function (error) { // Handle any error from all above steps }) done();
If we were to rewrite the Three.js library, we could have used promises in Three.js internally, but since Three.js already uses callbacks, we had to use the Q.defer()
function provided by Q to convert these callbacks to promises.
There is more...
We only touched a small part of what is possible with the Q promises library. We used it for synchronous loading, but Q has many other useful features. A very good starting point is the Q wiki available at https://github.com/kriskowal/q/wiki.
See also
- Just like every recipe that loads resources you have to make sure that you run it either with a local web server, see the Setting up a local web server using Python recipe or the Setting up a web server using Node.js recipe, or disable some security settings (see the Solving cross-origin-domain error messages in Chrome recipe or the Solving cross-origin-domain error messages in Firefox recipe). If you want to load resources asynchronously, you can take a look at the Load any resource asynchronously recipe.