A large number of the objects we manipulate have a tree structure. First and foremost the DOM tree and the accessibility tree; both of which can be fairly large. Therefore, we need an efficient way to represent them.
Because DOM tree is also our primary source of data that needs to be stored and exchanged with other parts of the system, it also needs to be easily serializable and deserializable.
A common operation we have to do on trees is traversal, both downward and upward. Especially, several rules in the audits search for an ancestor with a given property.
The easiest way to have upward traversal is to keep parent pointers in tree nodes. However, this effectively creates circular reference between a node and its children which is hard to serialize efficiently.
We use trees with parent pointers. But we drop the parent pointer upon serialization. To be able to accurately rebuild a tree upon deserialization, nodes have a method Node#_attachParent
which let the node be "adopted". To preserve (partial) immutability and prevent detachment, nodes are "frozen" upon adoption, and further adoption become impossible.
Accepted.
Since children list is immutable, the only time a node can adopt children is at creation time. That is, it is not possible to add a child to an existing node.
The freezing mechanism guarantees that a tree is downward immutable: it is possible to add a parent to a node, but never to change any of its descendants. We can leverage this property, for example to memoize operations performed on a full Document
tree.
Since parent pointers are added upon adoption, after the node is built (but before freezing it), there is no need for passing the parent when building the node; this allows for terminal recursion and space effective deserialization (notably avoiding the risk of hitting the recursion depth limit).
Nodes are not fully immutable anymore. The mutating properties and methods are kept private within the class. But other instance methods must take care about not accidentally creating hidden side effects (which would break ADR 6).
See more discussion about this choice in "The great refactoring" pull request, and the later discussion that led to the freezing mechanism being accepted.