javascriptevent-loopweb-standards

Enqueuing function to run after all events on current form are handled


I have a form with a formdata event handler that updates a field of the FormData if a certain state is found in the DOM. After the form submission, the DOM needs to be changed, but the formdata event handler should still observe the old state. Currently, I achieve this by using setTimeout(() => {/*...*/}, 0) in a submit event handler on the same form, because this will enqueue the function to run later - hopefully, after the formdata event is handled. My question is: Is this behavior guaranteed, and if not, is there a specification-backed way to accomplish this behavior?

In the specification of event loops, the first step for choosing what work to do next is described as:

Let taskQueue be one of the event loop's task queues, chosen in an implementation-defined manner [...]

The oldest task from this chosen queue is then run later on. This would mean, that if functions scheduled with setTimeout are in the same task queue as the event handlers and if the formdata event handler is scheduled before the submit handler is actually run, I would definitely be safe - I cannot find any evidence for that though. From the documentation of the formdata event ("fires after the entry list representing the form's data is constructed") and the fact, that the formdata handler is run after the submit handler, I would even assume the contrary to be true - but that is not what I observed with the approach described above.


Solution

  • Your understanding is quite correct, and you are right that setTimeout(fn, 0) may not always fire after a "related" event: it is indeed very possible that these events are fired from two different tasks, very unlikely they'll use the timer task sources, and you correctly identified the bit that would make the event loop "possibly" select a task from an other task source.

    However in this exact case, you are safe.

    The submit event and the formdata one are fired from the same "task"*.

    If you look at the form submit algorithm, you can see that the submit event is directly fired at step 6.5, instead of being wrapped in a task that would get queued like it's often the case.

    Let shouldContinue be the result of firing an event named submit at form [...]

    Then in the same algorithm, without any in parallel or anything implying asynchronicity, we have the step 8 that says

    Let entry list be the result of constructing the entry list with form, submitter, and encoding.

    And in this constructing the entry list algorithm, at the step 7, we have the call to

    Fire an event named formdata at form [...]

    once again without any asynchronicity allowed.

    So we can be sure that these events will fire without anything else in between (apart microtasks), and that your timer callback from the submit event will fire after the formdata callback, even two requestAnimationFrame callbacks scheduled in the same frame (that are also "synchronous" for the event loop) won't be able to interleave there:

    document.forms[0].addEventListener("submit", e => {
      console.log("submit");
    });
    document.forms[0].addEventListener("formdata", e => {
      console.log("formdata");
    });
    requestAnimationFrame(() => {
      console.log("rAF 1");
      document.forms[0].querySelector("button").click();
    });
    requestAnimationFrame(() => {
      console.log("rAF 2");
    });
    <form target="target">
      <input value="foo" name="bar">
      <button>
      submit
      </button>
    </form>
    <iframe name="target"></iframe>

    *Technically it does not even need to be from a task per se, you can very well force these events to fire from a microtask or a resize event callback etc.)