Skip to content

DOM architecture improvements #1

@bcardarella

Description

@bcardarella
graph TD
    A["Document
    (GenServer)
    PID: #PID<0.120.0>"] --> B["vml
    (Element GenServer)
    PID: #PID<0.121.0>"]
    
    B --> C["head
    (Element GenServer)
    PID: #PID<0.122.0>"]
    
    B --> D["body
    (Element GenServer)
    PID: #PID<0.123.0>"]
    
    D --> F["Text
    (Element GenServer)
    PID: #PID<0.125.0>"]
    
    F --> H["'Hello World'
    (Text GenServer)
    PID: #PID<0.127.0>"]

    classDef document fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    classDef element fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
    classDef text fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
    
    class A document
    class B,C,D,F element
    class H text
Loading

The core concept that the DOM tree is compirsed of Document that contains Nodes. Nodes can be Document, Element, or Text (currently).

The current implementation

As of this writing I implemented this quickly with the following:

When a new DOM is created each child will first be created. Document, Element, and Text are all valid nodes as they use GenDOM.Node. When a new Node is created a name is autogenerated for the GenServer. (I am considering dropping this in favor or just pure PIDs soon)

Then a Node should "join" a parent through one of the following:

  • append_child
  • insert_before
  • replace_child

When these functions are called the child is added in the correct position in child_nodes. The node is then passed to the parent's track callback which will Process.monitor the child and add its name to the all_child_names MapSet field of the state. Then GenServer.cast(parent.parent, {:track, child}) and the child is passed up the tree where each parent is going to track all of it's descendants. We do this so that we can implement faster querying for nodes. I suspect this will also be beneficial for the DOM Event system and event bubbling.

Process groups?

Internally the LVN team has discussed the idea of using Process groups instead of the above implementation. Where each parent is a group leader, and all children are in the group of each. However, the Elixir implementation of this doesn't permit multiple groups. And the :pg Erlang implementation doesn't guarantee ordering, let alone allow for node insertion at certain indices or replacing children. Looking at the Erlang implementation of :pg and I can easily replicate and extend this in Elixir for a custom Process group implementation that supports our needs: https://github.com/erlang/otp/blob/OTP-28.0/lib/kernel/src/pg.erl#L703-L718

However, would this be any better? I don't yet have benchmarks on the current implementation and so far it meets our needs. But it may not be the most idiomatic way to go about this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions