Skip to content

Commit

Permalink
Introduce DOM post-connection steps
Browse files Browse the repository at this point in the history
For any given insert operation, these steps run for each inserted node synchronously after all node insertions are complete. This closes whatwg#732.

The goal here is to separate the following:

1. Script-observable but not-script-executing insertion side effects
    - This includes synchronously applying styles to the document, etc...
2. Script-executing insertion side effects
    - This includes creating an iframe's document and synchronously firing its load event

For any given call to insert, the above model allows us to keep all of (1) running synchronously after each node's insertion (as part of its insertion steps), while pushing all script-executing (or DOM tree-modifying or frame tree-modifying etc.) side effects to the new set of post-connection steps, which run synchronously during insertion, but _after all_ nodes finish their insertion.

This essentially makes insertions "atomic" from the perspective of script, since script will not run until a given batch of DOM insertions are complete. This two-stage approach aligns the spec with a model most similar to Blink & Gecko's implementation, and fixes whatwg#808. This PR also helps out with whatwg/html#1127 and whatwg#575 (per whatwg#732 (comment)).

To accomplish, this we audited all insertion side effects on the web platform in https://docs.google.com/document/d/1Fu_pgSBziVIBG4MLjorpfkLTpPD6-XI3dTVrx4CZoqY/edit#heading=h.q06t2gg4vpw, and catalogued whether they have script-observable side-effects, whether they invoke script, whether we have tests for them, and how each implementation handles them. This gave us a list of present "insertion steps" that should move to the "post-connection steps", because they invoke script and therefore prevent insertions from being "atomic". This PR is powerless without counterpart changes to HTML, which will actually _use_ the post-connection steps for all current insertion steps that invoke script or modify the frame tree. The follow-up HTML work is tracked here:

- whatwg/html#10188
- whatwg/html#10241
  • Loading branch information
domfarolino authored and vinhill committed Jun 20, 2024
1 parent 3cb3151 commit cda03ae
Showing 1 changed file with 83 additions and 2 deletions.
85 changes: 83 additions & 2 deletions dom.bs
Original file line number Diff line number Diff line change
Expand Up @@ -2654,12 +2654,69 @@ of a <var>node</var> into a <var>parent</var> before a <var>child</var>, run the

<p><a lt="Other applicable specifications">Specifications</a> may define
<dfn export id=concept-node-insert-ext>insertion steps</dfn> for all or some <a for=/>nodes</a>. The
algorithm is passed <var ignore>insertedNode</var>, as indicated in the <a for=/>insert</a>
algorithm below.
algorithm is passed <var>insertedNode</var>, as indicated in the <a for=/>insert</a> algorithm
below. These steps must not modify the <a>node tree</a> that <var>insertedNode</var>
<a>participates</a> in, create <a for=/>browsing contexts</a>,
<a lt="fire an event">fire events</a>, or otherwise execute JavaScript. These steps may
[=queue a global task|queue tasks=] to do these things asynchronously, however.

<div class=example id=example-foo-what-do-i-put-here>
<p>While the <a>insertion steps</a> cannot execute JavaScript (among other things), they will
indeed have script-observable consequences. Consider the below example:

<pre class=lang-javascript><code>
const h1 = document.querySelector('h1');

const fragment = new DocumentFragment();
const script = fragment.appendChild(document.createElement('script'));
const style = fragment.appendChild(document.createElement('style'));

script.innerText= 'console.log(getComputedStyle(h1).color)'; // Logs 'rgb(255, 0, 0)'
style.innerText = 'h1 {color: rgb(255, 0, 0);}';

document.body.append(fragment);
</code></pre>

<p>The script in the above example logs <code class=lang-javascript>'rgb(255, 0, 0)'</code> because
the following happen in order:

<ol>
<li><p>The <a for=/>insert</a> algorithm runs, which will insert the <{script}> and <code>
<a element spec=HTML>style</a></code> elements in order.

<ol>
<li><p>The HTML Standard's <a>insertion steps</a> run for the <{script}> element; they do
nothing. [[!HTML]]

<li><p>The HTML Standard's <a>insertion steps</a> run for the <code>
<a element spec=HTML>style</a></code> element; they immediately apply its style rules to the
document. [[!HTML]]

<li><p>The HTML Standard's <a>post-connection steps</a> run for the <{script}> element; they run
the script, which immediately observes the style rules that were applied in the above step.
[[!HTML]]
</ol>
</li>
</ol>
</div>

<!-- See https://github.com/whatwg/dom/issues/34#issuecomment-125571750 for why we might need to
adjust this further based on the requirements of the script element. There might be other ways
to define that though as Olli suggests, so leaving that out for now. -->

<p><a lt="Other applicable specifications">Specifications</a> may also define
<dfn export id=concept-node-post-connection-ext>post-connection steps</dfn> for all or some
<a for=/>nodes</a>. The algorithm is passed <var ignore>connectedNode</var>, as indicated in the
<a for=/>insert</a> algorithm below.

<p class=note>The purpose of the <a>post-connection steps</a> is to provide an opportunity for
<a for=/>nodes</a> to perform any connection-related operations that modify the <a>node tree</a>
that <var ignore>connectedNode</var> <a>participates</a> in, create <a for=/>browsing contexts</a>,
or otherwise execute JavaScript. These steps allow a batch of <a>nodes</a> to be
<a for=/>inserted</a> <em>atomically</em> with respect to script, with all major side effects
occurring <em>after</em> the batch insertions into the <a>node tree</a> is complete. This ensures
that all pending <a>node tree</a> insertions completely finish before more insertions can occur.

<p><a lt="other applicable specifications">Specifications</a> may define
<dfn export id=concept-node-children-changed-ext>children changed steps</dfn> for all or some
<a for=/>nodes</a>. The algorithm is passed no argument and is called from <a for=/>insert</a>,
Expand Down Expand Up @@ -2761,6 +2818,30 @@ before a <var>child</var>, with an optional <i>suppress observers flag</i>, run
<var>parent</var> with <var>nodes</var>, « », <var>previousSibling</var>, and <var>child</var>.

<li><p>Run the <a>children changed steps</a> for <var>parent</var>.

<li>
<p>Let <var>staticNodeList</var> be a <a for=/>list</a> of <a for=/>nodes</a>, initially « ».</p>

<p class="note">We collect all <a for=/>nodes</a> <em>before</em> calling the
<a>post-connection steps</a> on any one of them, instead of calling the
<a>post-connection steps</a> <em>while</em> we're traversing the <a>node tree</a>. This is because
the <a>post-connection steps</a> can modify the tree's structure, making live traversal unsafe,
possibly leading to the <a>post-connection steps</a> being called multiple times on the same
<a>node</a>.</p>
</li>

<li>
<p>For each <var>node</var> of <var>nodes</var>, in <a>tree order</a>:

<ol>
<li><p>For each <a>shadow-including inclusive descendant</a> <var>inclusiveDescendant</var> of
<var>node</var>, in <a>shadow-including tree order</a>, <a for=list>append</a>
<var>inclusiveDescendant</var> to <var>staticNodeList</var>.
</ol>
</li>

<li><p><a for=list>For each</a> <var>node</var> of <var>staticNodeList</var>, if <var>node</var> is
<a>connected</a>, then run the <a>post-connection steps</a> with <var>node</var>.
</ol>


Expand Down

0 comments on commit cda03ae

Please sign in to comment.