1

So, basically, I want to do is:

  1. Get an Event from an event listener,
  2. handling that event,
  3. waiting for it to finish,
  4. and then pass to another in the queue,

so if I get like 15 an event 15 times, I don't want to be handled all at the same time but more like event handle-await seconds event. I've tried like this to have one printed, then 5 seconds, then two with multiple clicks but what I've got was a lot of "one" at the same time and then a cascade of "two."

document.addEventListener("click", async function(event) {
    if (!event.target.matches(".button-name"));
    console.log("one");
    await sleep(5000);
    console.log("two");
    event.preventDefault();
}, );

async function sleep(milliseconds) {
    return new Promise((resolve) = \ > setTimeout(resolve, milliseconds));
}
Albert Einstein
  • 7,472
  • 8
  • 36
  • 71
CursingOwl
  • 11
  • 1
  • Javascript is single-threaded. Only one part of your code will be active at any time. If you (ab)use the method you've used to "sleep" your code, then that code is not running, so the next code can continue. – freedomn-m Jul 04 '23 at 15:00
  • There's likely a better way that the `sleep` method you've employed to achieve what you're actually trying to do, if this is for a real application (rather than purely academic). – freedomn-m Jul 04 '23 at 15:01
  • 2
    The key to this question is the 'queue'. When your handler receives an event, it should add an item to a queue, or in other words, an array. Another function can then iterate over each item in the array until the array is empty. If you can use an external library, RxJS Observables are great for this https://rxjs.dev/guide/observable. Otherwise you just have to handle the logic for adding and removing items from the array manually in a first in, first out way. – Stuart Nichols Jul 04 '23 at 15:08
  • Consider putting the work in an array as a queue – Daniel A. White Jul 04 '23 at 15:08

3 Answers3

0

To fix this problem, you need to use an async generator function that yields each click event as it occurs.

For example:

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

async function* getClickEvents() {
  let resolve;
  let promise = new Promise((r) => {
    resolve = r;
  });
  document.addEventListener("click", (event) => {
    resolve(event);
  });
  while (true) {
    const value = await promise;
    yield value;
    promise = new Promise((r) => {
      resolve = r;
    });
  }
}

async function handleClickEvents() {
  for await (const event of getClickEvents()) {
    if (event.target.matches(".button-name")) {
      console.log("one");
      await sleep(5000);
      console.log("two");
      event.preventDefault();
    }
  }
}

handleClickEvents();

You can learn more about how JavaScript handles events and callbacks using the event loop and the queue from these sources:

Pluto
  • 4,177
  • 1
  • 3
  • 25
0

A fairly simple solution would be to have a variable hold a promise. We can then use this promise as a queue. Whenever the event handler is called, we can chain a new task to this queue.

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

let queue = Promise.resolve();
document.addEventListener("click", function (event) {
  queue = queue.then(async function () {
    console.log("one");
    await sleep(5000);
    console.log("two");
  });
});

There are a few important things to note here.

  1. The above doesn't handle exceptions. Whenever one of the queued tasks fails the entire queue will stop. To prevent this make sure each added task has its own catch mechanism.

    queue = queue.then(...).catch(/* handle exception */);
    
  2. event.preventDefault() can only be called as direct response to an event. If you call it in an queued function, the window of opportunity is gone so it will not have any meaningful effect.


In the snippet above queue is visible to other code within the same scope. If you do not want this you can wrap the above handler in an IIFE.

document.addEventListener("click", (function () {
  let queue = Promise.resolve();
  return function (event) {
    queue = queue.then(...);
  };
})());
3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
0

Why it doesn't work.

When you execute an await operator, the execution context is copied and saved in heap memory, and execution of the next lower JavaScript execution context on the call stack is resumed, or if there is none, control returns to the event loop.

If await is used in an event handler, the handler is called via the event loop and control returns to the event loop immediately when an await operator is executed. This allows more events to occur, which is why you get multiple "ones" from events as they occur, and then a lot of "twos" as the sleep promises become settled after a delay.

You could put data from Event objects into a queue, or possibly event objects themselves1, and process them asynchronously after being decoupled from the event processing model. Some design and experimentation would be called for if events have default actions.

Depending on the aim of the code there may be alternatives to the queue as well: if throttling events is the goal, meaning to ignore new events if a previous one of the same kind is being processed, then some state based processing as opposed to a queue could be used. As always, a final design would depend on real application requirements.


1 Never use window.event in event handlers. It comes from the design of Internet Explorer and Microsoft's JScript architecture. It can be overwritten if events occur while an event handler is awaiting something during asynchronous code execution.

traktor
  • 17,588
  • 4
  • 32
  • 53