Is it possible to use a sync promise sleep inside of jQuery.each()?

Viewed 523

So basically. I want to use jQuery's each function within a sync function. However, as far as I know. It doesn't work because the each function just checks if the return value is false to break the loop. It doesn't check if its a promise and then sets it to await for it to be resolved. Is there a simple way to make the each function of jQuery synchronous with promises?

I currently have an alternative which is to create a for loop iterating the elements, but its a bit lengthy to write so I'd like to avoid that if possible.

Here is a fiddle of it: https://jsfiddle.net/3pcvqswn/2/

function sleep(ms)
{
    return new Promise(resolve => setTimeout(resolve, ms));
}

$(document).ready(function()
{
    (async () =>
  {
    console.log("This method works, but its lengthy to write");
    var elements = $("div");

    for(var index = 0; index < elements.length; index++)
    {
        var el = $(elements.eq(index));

        console.log("The content is: " + el.text());

      await sleep(1000);
        }

    console.log("This method doesn't work, but its easier to write");
    $("div").each(async (index, el) =>
    {
        console.log("The content is: " + $(el).text());

      // doesn't wait
      await sleep(1000);
    });
  })();
});

I decided to go with for of, but a more accurate solution to have both the index and element callback would be

  Object.entries($("div")).forEach(async ([index, el]) =>
  {
    console.log("row ", index, el);
  });
3 Answers

No - jQuery's .each works similarly to forEach. Each callback will be fired off one after the other, and if the callback returns a Promise (such as if it's an async function), it won't be waited for - it works similarly to

callback();
callback();
callback();

Unless something in the callback blocks (which it shouldn't if you're using decent programming practices), nothing asynchronous initialized in the callback can result in further callbacks waiting for that asynchronous action to complete.

But jQuery objects are iterable, so it's trivial to use a concise for..of to iterate over the elements:

const sleep = ms => new Promise(res => setTimeout(res, ms));

(async () => {
  for (const elm of $("div")) {
    console.log("The content is: " + $(elm).text());
    await sleep(1000);
  }
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div>text 1</div>
<div>text 2</div>
<div>text 3</div>

That's not verbose at all IMO.

In order to use await in a loop, the loop either has to be a for loop (either for..in, for..of, or for(let i = ...), or the loop's iteration mechanism needs to be constructed so that it waits for the last callback to resolve, such as with reduce:

const sleep = ms => new Promise(res => setTimeout(res, ms));

[...$("div")].reduce(async (lastProm, elm) => {
  await lastProm;
  console.log("The content is: " + $(elm).text());
  await sleep(1000);
}, Promise.resolve());
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div>text 1</div>
<div>text 2</div>
<div>text 3</div>


Side note that's barely worth mentioning - I said earlier that using await inside .each won't work

Unless something in the callback blocks (which it shouldn't if you're using decent programming practices)

If you write an expensive and silly blocking version of sleep which consumes all resources until the next second is reached, it's "possible" to wait inside the loop, but it shouldn't ever be done:

const sleep = ms => {
  const now = Date.now();
  while (Date.now() - now < ms) {}
}

It's be much better to work with the Promises properly.

You can't force $.each to run serially (one after another). It runs the function for each element "simultaneously". One option is to write your own each function that just performs the longer version you wrote in your question:

const serialEach = async (f, elements) => {
   for(let index = 0; index < elements.length; index++) {
       const el = $(elements.eq(index))

       await f(el)
    }
}

Then you could run it like this:

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
}

$(document).ready(() => {
    const elements = $("div")

    serialEach(async (el) => {
        console.log("The content is: " + el.text())

        await sleep(1000)
    }, elements)
})

Note that serialEach returns a promise, so it will return immediately and any code you have after the serialEach call will execute, even while serialEach is iterating over your elements. If you want to wait for that iteration to complete before continuing, you could just await serialEach (and make your $.ready callback async).

I wouldnt advice to use .each for this. the function gets executed regarless if it's a asynchronous or not. But with programming you can be creative.

//code

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

let promise = Promise.resolve();
$("div").each((index, el) => {
  promise = promise.then(async () => {
    console.log("The content is: " + $(el).text());

    await sleep(1000);
  });
});

await promise;

//code
Related