skip to Main Content
  1. click lock 5s button: JavaScript enters a dead loop. Browser main thread blocked

  2. Within five seconds of being locked, enter any amount of text in the input or click the btn button any number of times.

  3. Waiting for unlocking. Then you will find that the console suddenly prints a lot of text.

My expectation is that user actions will not be responded to during the blocking process. When the lock is released, the console only outputs start lock and end lock. But in fact, it’s not like that, so I’m very confused.

<!DOCTYPE html>
<html lang="en">

<head></head>

<body>
  <input id="input" type="text" />
  <button id="lock">lock 5s</button>
  <button id="btn">btn</button>
  <script>
    document.querySelector("#input").addEventListener("input", () => {
      console.log("on input", input.value);
    });

    document.querySelector("#lock").addEventListener("click", () => {
      console.log("start lock");
      const start = performance.now();
      while (performance.now() - start < 5000) continue;
      console.log("end lock");
    });
    document.querySelector("#btn").addEventListener("click", () => {
      console.log("btn");
    });
  </script>
</body>

</html>

2

Answers


  1. As mentioned in the comments the events are queued.

    An interesting case that shows that events are processed after the macrotask is complete.

    When the lock is over we add a DIV that pushes the button down.

    Then the queued events are processed against the updated DOM (without re-rendering).
    Since there’s no button anymore under the points the mouse was clicked the mouse events goes into the body.

    <input id="input" type="text" />
      <button id="lock">lock 5s</button>
      <button id="btn">btn</button>
      <script>
        document.querySelector("#input").addEventListener("input", () => {
          console.log("on input", input.value);
        });
    
        document.addEventListener('click', e => e.target.id !== 'lock' && console.log('body clicked', e.target));
    
        document.querySelector("#lock").addEventListener("click", () => {
          console.log("start lock");
          const start = performance.now();
          while (performance.now() - start < 5000) continue;    
          document.body.insertAdjacentHTML('beforebegin', '<div class="test">TEST</div>');  
          console.log("end lock");
        });
        document.querySelector("#btn").addEventListener("click", () => {
          console.log("btn");
        });
      </script>

    The seconds example make it more obvious that the events goes against the updated DOM, before any rendering:

    1. Click the lock btn
    2. Click in the space where the ‘btn’ was before jumping lower
    <input id="input" type="text" />
      <button id="lock">lock 5s</button>
      <button id="btn">btn</button>
      <script>
        document.querySelector("#input").addEventListener("input", () => {
          console.log("on input", input.value);
        });
        
        document.querySelector("#lock").addEventListener("click", async () => {
          console.log("start lock");
          document.body.insertAdjacentHTML('beforebegin', '<div class="test">TEST</div>');  
          await new Promise(r=>setTimeout(r));
          const start = performance.now();
          while (performance.now() - start < 5000) continue;    
          document.querySelector('.test').remove()
          console.log("end lock");
        });
        document.querySelector("#btn").addEventListener("click", () => {
          console.log("btn");
        });
      </script>
    Login or Signup to reply.
  2. Blocking the main JS thread is something you want to avoid..

    If your just wanting to disable user interaction for 5 seconds, one idea is just place a div full screen over your page, you could have it set to transparent.

    Alternatively you could even create a <dialog/> element informing the user, rather than locking up the UI. Doing this click events are still processed in the event loop, and then won’t stack as in your example.

    eg.

    const dialog = document.querySelector('dialog');
    const btnLock = document.querySelector('#lock');
    
    btnLock.addEventListener('click', () => {
      const eatClicks = e => e.stopPropagation();
      dialog.addEventListener('click', eatClicks);
      dialog.showModal();
      setTimeout(() => {
        dialog.close();
        dialog.removeEventListener('click', eatClicks);
      }, 5000);
    });
    
    document.addEventListener('click', e =>    console.log('body clicked', e.target));
    <input id="input" type="text" />
    <button id="lock">lock 5s</button>
    <button id="btn">btn</button>
    
    <dialog>Please wait..</dialog>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search