Don't run scripts while DOM tree is undergoing mutations (#34505)

* script: Implement node insertion post-connection hook. Ensure script elements only run scripts when the DOM has stabilized.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

* script: Make iframe element use post-connection steps when handling initial document insertion.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

* script: Use a delayed task when running post-connection steps.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

* script: Add explanatory comment.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

* Tidy.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

---------

Signed-off-by: Josh Matthews <josh@joshmatthews.net>
This commit is contained in:
Josh Matthews 2024-12-26 01:06:09 -05:00 committed by GitHub
parent 20d67bdc44
commit 981616f918
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 117 additions and 51 deletions

View file

@ -68,6 +68,7 @@ use crate::dom::bindings::inheritance::{
Castable, CharacterDataTypeId, ElementTypeId, EventTargetTypeId, HTMLElementTypeId, NodeTypeId,
SVGElementTypeId, SVGGraphicsElementTypeId, TextTypeId,
};
use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, DomObject, DomObjectWrap};
use crate::dom::bindings::root::{Dom, DomRoot, DomSlice, LayoutDom, MutNullableDom};
use crate::dom::bindings::str::{DOMString, USVString};
@ -2128,6 +2129,38 @@ impl Node {
};
MutationObserver::queue_a_mutation_record(parent, mutation);
}
// Step 10. Let staticNodeList be a list of nodes, initially « ».
let mut static_node_list = vec![];
// Step 11. For each node of nodes, in tree order:
for node in new_nodes {
// Step 11.1 For each shadow-including inclusive descendant inclusiveDescendant of node,
// in shadow-including tree order, append inclusiveDescendant to staticNodeList.
static_node_list.extend(
node.traverse_preorder(ShadowIncluding::Yes)
.map(|n| Trusted::new(&*n)),
);
}
// We use a delayed task for this step to work around an awkward interaction between
// script/layout blockers, Node::replace_all, and the children_changed vtable method.
// Any node with a post connection step that triggers layout (such as iframes) needs
// to be marked as dirty before doing so. This is handled by Node's children_changed
// callback, but when Node::insert is called as part of Node::replace_all then the
// callback is suppressed until we return to Node::replace_all. To ensure the sequence:
// 1) children_changed in Node::replace_all,
// 2) post_connection_steps from Node::insert,
// we use a delayed task that will run as soon as Node::insert removes its
// script/layout blocker.
node.owner_doc().add_delayed_task(task!(PostConnectionSteps: move || {
// Step 12. For each node of staticNodeList, if node is connected, then run the
// post-connection steps with node.
for node in static_node_list.iter().map(Trusted::root).filter(|n| n.is_connected()) {
vtable_for(&node).post_connection_steps();
}
}));
node.owner_doc().remove_script_and_layout_blocker();
}