JavaScript Canvas - Introduction To Bitmaps
Written by Ian Elliot   
Monday, 29 June 2020
Article Index
JavaScript Canvas - Introduction To Bitmaps
Async Loading
Drawing an Image

Asynchronous Loading

The reason is that you are trying to use properties of the image that are only available after the image has loaded. When you first encounter this sort of behavior, especially if you are used to other languages, the tendency is to think that the quick fix is to put in a delay to give the image time to load. For JavaScript this doesn't work because any delay that you put in, by whatever method, freezes the UI and many other actions. That is, if you wait for the image load with a delay, the image stops loading until the delay is complete.

For example, a delay function is easy to create:

function delay(ms) {
   var curTicks = Date.now();
   var endTicks = curTicks + ms;
   while (curTicks < endTicks) {
       curTicks = Date.now();
   }
}

This keeps the processor busy while waiting for the specified number of milliseconds to pass. If you put a call to delay in the image load to create a delay of 10 seconds:

var img1 = new Image();
img1.src = "jeep.jpg";
delay(10000);
img1.width = img1.naturalWidth / 10;
img1.height = img1.naturalHeight / 10;
document.body.appendChild(img1);

you will most likely discover that the image hasn't loaded after the delay. Even if it has, the browser UI will have frozen for the full 10 seconds, which is unacceptable.

The traditional correct solution is to define a load event handler:

var img1 = new Image();
img1.src = "jeep.jpg";
img1.addEventListener("load",
           function () {
              img1.width = img1.naturalWidth / 10;
              img1.height = img1.naturalHeight / 10;
              document.body.appendChild(img1);
           });

Now everything works but you have the complication of an event handler. This is more complicated and subtle than it looks. For example, the event will occur long after the main program has completed and yet the event handler makes reference to img1, which is a variable that shouldn't exist after the main program ends. The only reason the event function can make use of it is due to closure. For an explanation, see my book JavaScript Async: Events, Callbacks, Promises and Async Await (ISBN: 978-1871962567).

coverasync

Setting an event handler in this way is the most basic way of dealing with the problem of waiting for the image to load. A more modern way is to make use of a Promise object. To do this we have to write a function which wraps the onload event:

function imgLoaded(img) {
  return new Promise(
           function (resolve,reject) {
               img.addEventListener("load", function () {
                                          resolve(img);
                                         });
           }
        );
}

This immediately returns a Promise object which calls the resolve function when the image is loaded. Using this you would write the previous example as:

var img1 = new Image();
img1.src = "jeep.jpg";
imgLoaded(img1).then(function (img) {
                   img.width = img.naturalWidth / 10;
                   img.height = img.naturalHeight / 10;
                   document.body.appendChild(img);
                     });

Notice that now the code to be executed when the image is loaded is in the then function of the promise. Also notice that we are using the parameter returned by the promise i.e. img rather than img1. The advantage of this approach is that you can chain multiple promises when loading multiple images and arrange to use the reject function to handle errors.

Promises are an improvement on event handling and callbacks but the very best way of implementing this is to use async/await and as these are now implemented by every browser that implements promises there is no reason not to use it. The only problem is that you cannot await a function from the main program and this means it is better to write a function that loads the image and awaits it:

async function getImage(url) {
    var img = new Image();
    img.src = url;
    await imgLoaded(img);
    img.width = img.naturalWidth / 10;
    img.height = img.naturalHeight / 10;
    document.body.appendChild(img);
}

Now the await causes the function to return to the main program. It is restarted at the instruction after the await as soon as the image is loaded.

If you can use async and await, this is the best way to work. It preserves the look of the code that you would write if the image was loaded instantly and it throws an exception if there is an error. This means you can put the await in a try-catch and handle errors in the usual way.



Last Updated ( Monday, 29 June 2020 )