0

I am making a easy html5 game.

        Object.keys(gameConfig.playerElems).map((e) =>{

        let img = gameConfig.playerElems[e];
        let name = e;
        let imgObj;

        imgObj = new Image();
        imgObj.src = img;

        imgObj.onload = () => {
            playerElemsCounter++;
            drawPlayer(imgObj);
        }
    });

Is it possible to pause .map() iteration while imgObj will be loaded?

Brian Mains
  • 50,520
  • 35
  • 148
  • 257
Dominik
  • 1,265
  • 1
  • 16
  • 27
  • 1
    You cannot. You can though use recursion to achieve this – Rajesh Aug 29 '17 at 10:36
  • It's not possible, since JS won't fire the events until you return to the event loop, even if the image gets loaded. You will have to redesign your code in a asynchronous way. – Haroldo_OK Aug 29 '17 at 10:41
  • 2
    With some minor adjustments the ES6 part of the solution in this question would also work: [Callback after all asynchronous forEach callbacks are completed](https://stackoverflow.com/questions/18983138/callback-after-all-asynchronous-foreach-callbacks-are-completed) – Andreas Aug 29 '17 at 10:45

1 Answers1

2

Is it possible to pause .map() iteration while imgObj will be loaded?

No. So instead, you use an asynchronous loop. Here's one example, see comments:

// A named IIFE
(function iteration(keys, index) {
    // Get info for this iteration
    let name = keys[index];
    let img = gameConfig.playerElems[name];
    let imgObj = new Image();
    // Set event callbacks BEFORE setting src
    imgObj.onload = () => {
        playerElemsCounter++;
        drawPlayer(imgObj);
        next();
    };
    imgObj.onerror = next;
    // Now set src
    imgObj.src = img;

    // Handles triggering the next iteration on load or error
    function next() {
        ++index;
        if (index < keys.length) {
            iteration(keys, index);
        }
    }
})(Object.keys(gameConfig.playerElems), 0);

But, as Haroldo_OK points out, this will wait for one image to load before requesting the next, which is not only unnecessary, but harmful. Instead, request them all, draw them as you receive them, and then continue. You might do that by giving yourself a loading function returning a promise:

const loadImage = src => new Promise((resolve, reject) => {
    const imgObj = new Image();
    // Set event callbacks BEFORE setting src
    imgObj.onload = () => { resolve(imgObj); };
    imgObj.onerror = reject;
    // Now set src
    imgObj.src = src;
});

Then:

// Load in parallel, draw as we receive them
Promise.all(Object.keys(gameConfig.playerElems).map(
    key => loadImage(gameConfig.playerElems[key])
            .then(drawPlayer)
            .catch(() => drawPlayer(/*...placeholder image URL...*/))
)
.then(() => {
    // All done, if you want to do something here
});
// No need for `.catch`, we handled errors inline

If you wanted (for some reason) to hold up loading the next image while waiting for the previous, that loadImage function could be used differently to do so, for instance with the classic promise reduce pattern:

// Sequential (probably not a good idea)
Object.keys(gameConfig.playerElems).reduce(
    (p, key) => p.then(() =>
                    loadImage(gameConfig.playerElems[key])
                    .then(drawPlayer)
                    .catch(() => drawPlayer(/*...placeholder image URL...*/))
                )
    ,
    Promise.resolve()
)
.then(() => {
    // All done, if you want to do something here
});
// No need for `.catch`, we handled errors inline

...or with ES2017 async/await:

// Sequential (probably not a good idea)
(async function() {
    for (const key of Object.keys(gameConfig.playerElems)) {
        try {
            const imgObj = await loadImage(gameConfig.playerElems[name]);
            playerElemsCounter++;
            drawPlayer(imgObj);
        } catch (err) {
            // use placeholder
            drawPlayer(/*...placeholder image URL...*/);
        }
    }
})().then(() => {
    // All done
});
// No need for `.catch`, we handled errors inline

Side note: There's no point to using map if you're not A) Returning a value from the callback to use to fill the new array map creates, and B) Using the array map returns. When you're not doing that, just use forEach (or a for or for-of loop).

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 1
    Another way would be ask the browser to load all images at once, and then wait until all the `onload` events have happened. This would have the advantage that if one specific image loads more slowly than the others, it won't hold back the queue. – Haroldo_OK Aug 29 '17 at 10:45
  • 1
    @Haroldo_OK: That would be a much better way to do it. I was very literal in my interpretation of the OP's code, wasn't I? Doh! – T.J. Crowder Aug 29 '17 at 10:48
  • @Andreas' comment is closer to how I would implement it (with `Promise.all()`): https://stackoverflow.com/questions/18983138/callback-after-all-asynchronous-foreach-callbacks-are-completed – Haroldo_OK Aug 29 '17 at 10:48
  • 1
    @Haroldo_OK: Added parallel option, and promise-based sequential ones (just in case). – T.J. Crowder Aug 29 '17 at 10:58
  • What is 'img' in " imgObj.src = img;" ? And what is 'src' in const loadImage = src = new Promise((resolve, reject) ? – Dominik Aug 29 '17 at 12:25
  • 1
    @Dominik `imgObj.src = img;` is from your script. In the second part of your comment, you've missed the `>`. It's `const loadImage = src => new Promise(...)` which is (in this case) the same as: `function loadImage(src) { return new Promise(...); }` -> [Arrow function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) – Andreas Aug 29 '17 at 12:48