1

I have an onload function within a for statement that is producing unexpected results.

The for loop loops through an array of images. I'm wanting this loop to execute an onload for each image in the array, and only proceed to the next iteration in the for loop once that's done. The onload is waiting for an image element to load before it executes.

  for (index = 0; index < lightboxImages.length; index++) {
    console.log('index :' + index);
    // when the current image has finished loading
    lightboxImages[index].onload = function() {
      console.log('image ' + index + ' loaded.');
    };
  }

There are only three elements in the array, and with my console.log statements I'm expecting something like this in the console:

index : 0
image 0 loaded.
index : 1
image 1 loaded.
index : 2
image 2 loaded.

But instead I'm getting output like this:

index : 0
index : 1
index : 2
image 3 loaded.
image 3 loaded.
image 3 loaded.

How can I prevent iteration until the onload is finished?

I also tried something like this:

// just an index variable as an example
var counter = 2;

toImage(i) {
  if (i == counter) {
    console.log('hooray');
  }
  else {
    toImage(i++);
  }
}

toImage(0);

This was causing a callstack issue that was crashing the page - I didn't include it in the original post, because I figured it was a whole separate issue, but maybe I should have in retrospect. I guess my attempt at recursion here is wrong.

rpivovar
  • 3,150
  • 13
  • 41
  • 79
  • Assigning to `.onload` does install an event handler, it doesn't block and wait until that event has occurred. – Bergi Mar 20 '18 at 22:06
  • This isn't possible as written. You'll need some kind of queue or recursion. (that said.... if you're just preloading images, i suggest looking for a script, there's hundreds that already do this.) – Kevin B Mar 20 '18 at 22:07
  • 1
    See [this question](https://stackoverflow.com/q/750486/1048572) on why you are getting `image 3` three times. Maybe that helps you to get a solution that doesn't need to stop iterating? – Bergi Mar 20 '18 at 22:07
  • 1
    I did try a recursive function, but that was causing me problems, too - I'll add that to my code above – rpivovar Mar 20 '18 at 22:08
  • Thanks @Bergi I'm checking this out – rpivovar Mar 20 '18 at 22:10
  • @KevinB thanks for the dupe question, checking that one out, too – rpivovar Mar 20 '18 at 22:13
  • 1
    here's another, though dunno if the answer works https://stackoverflow.com/questions/6702735/preload-images-with-queuing – Kevin B Mar 20 '18 at 22:14
  • 1
    And here's a much older, more comprehensive/popular thread on the topic: https://stackoverflow.com/questions/901677/the-definitive-best-way-to-preload-images-using-javascript-jquery – Kevin B Mar 20 '18 at 22:15
  • Thanks @KevinB, I added the recursion I previously tried above, but I guess I didn't do it right. I'll check out your resources – rpivovar Mar 20 '18 at 22:19
  • This is absolutely not a duplicate question, at least not for the question you linked. This question is about closure and scope, not image preloading. – Nick Coad Mar 20 '18 at 22:19
  • 1
    `++` -> `++i` you forgot `i`, and want to increment it before it gets passed as a parameter. – Kevin B Mar 20 '18 at 22:20
  • oops, sorry, it was originally `i++` in my code, I updated the question – rpivovar Mar 20 '18 at 22:20
  • definitely reading up on `++i` vs `i++`, thank you @KevinB – rpivovar Mar 20 '18 at 22:22
  • @rpivovar, please note that poster has misunderstood your question. His proposed solution will not fix your problem. – Nick Coad Mar 20 '18 at 22:24
  • @NickCoad I agree the preloading stuff is not exactly what I'm looking for, but the recursion fix is definitely helpful – rpivovar Mar 20 '18 at 22:24

4 Answers4

1

An alternative is recursive calls:

function loop(lightboxImages, index) {
    if (index === lightboxImages.length) return;

    lightboxImages[index].onload = function() {
      console.log('image ' + index + ' loaded.');
      loop(lightboxImages, ++index);
    };
}

loop(lightboxImages, 0);
Ele
  • 33,468
  • 7
  • 37
  • 75
1

You can solve this with a closure.

var someFakeImages = [
  { onload: function () {} },
  { onload: function () {} },
  { onload: function () {} }
];

var onloadFunc = function(i) {
    return function () {
        console.log('image ' + i + ' loaded.');
    }
};

for (index = 0; index < someFakeImages.length; index++) {
    console.log('index: ' + index);

    // when the current image has finished loading
    someFakeImages[index].onload = onloadFunc(index);
}

someFakeImages[0].onload();
someFakeImages[1].onload();
someFakeImages[2].onload();

You can find out more about closures on the MDN website: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

Basically with the above, you are creating a new function with a snapshot of the value of index at the time of assigning the onload handler. In your posted code, you are actually using the current value of index at the time of executing the onload handler.

Nick Coad
  • 3,623
  • 4
  • 30
  • 63
0

You console.log is showing the CURRENT value of index. You see, the loop has finished and index is now at 3 when the onloads are fired. You need to pass the index as a LOCAL variable to the onload handler.

It is a common 'issue' for many programmers in dealing with asynchronous coding ;)

Mr. de Silva
  • 382
  • 2
  • 11
0

Assigning to onload only assigns an event handler: it doesn't mean that the current thread stops and waits for the load to finish. I suggest using reduce and await instead.

function loadImages(lightboxImages) {
  lightboxImages.reduce(async (lastPromise, image, i) => {
    await lastPromise;
    return new Promise((resolve, reject) => {
      image.onload = () => {
        console.log(`image ${i} loaded.`);
        resolve();
      };
    });
  }, Promise.resolve());
}
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • 2
    Don't make it any more complicated than it needs to be with that `reduce`. If you are going to use `async`/`await` anyway, just loop directly (with `for (const [i, image] of lightboxImages.entries())`) and `await` each one. – Bergi Mar 20 '18 at 22:14