VanJS: Advanced Topics
DOM Attributes vs. Properties
In tag functions
, while assigning values from props
parameter to the created HTML element, there are 2 ways of doing it: via HTML attributes
(dom.setAttribute(<key>, <value>)
), or via the properties of the created HTML element (dom[<key>] = <value>
). VanJS follows a consistent rule that makes sense for most use cases regarding which option to choose: when a settable property exists in a given <key>
for the specific element type, we will assign the value via property, otherwise we will assign the value via attribute.
For instance, input({type: "text", value: "Hello 🍦VanJS"})
will create an input box with Hello 🍦VanJS
as the value of the value
property, while div({"data-index": 1})
will create the tag: <div data-index="1"></div>
.
Note that, for readonly properties of HTML elements, we will still assign props
values via setAttribute
. For instance, in the code snippet below, the list
of the <input>
element is set via setAttribute
:
const Datalist = () => div(
label({for: "ice-cream-choice"}, "Choose a flavor: "),
input({
list: "ice-cream-flavors",
id: "ice-cream-choice",
name: "ice-cream-choice",
}),
datalist(
{id: "ice-cream-flavors"},
option({value: "Chocolate"}),
option({value: "Coconut"}),
option({value: "Mint"}),
option({value: "Strawberry"}),
option({value: "Vanilla"}),
)
)
Garbage Collection
There is garbage collection mechanism implemented in VanJS to recycle obsolete state bindings. To illustrate the necessity of garbage collection, let's take a look at the code below:
const renderPre = van.state(false)
const text = van.state("Text")
const TextLine = (renderPre: boolean) =>
(renderPre ? pre : div)(
van.bind(text, t => `--${t}--`),
)
const dom = div(van.bind(renderPre, TextLine))
In this piece of code, we have created an element dom
, whose only child is bound to a boolean
state - renderPre
, which determines whether dom
has a <pre>
or <div>
child element. Inside the child element, the underlying text is bound to a string
state - text
. Whenever the value of renderPre
is toggled, a new version of the DOM tree will be generated, and we will add a new binding from text
state to the text node of the newly created DOM tree.
Without proper garbage collection implemented, text
state will eventually be bound to many text nodes after renderPre
is toggled many times. All the of bindings, except the most recently added one, are actually obsolete, as they bind the text
state to a text node that is not currently being used. i.e.: disconnected from the document tree. Meanwhile, because internally, a State
object holds reference to all DOM elements that are bound to it, these DOM elements won't be GC-ed by JavaScript runtime, causing memory leaks.
Garbage collection is implemented in VanJS to resolve the issue. There are 2 ways a garbage collection activity can be triggered:
- Periodic recycling: periodically, VanJS will scan all
State
objects that have new bindings added recently, and remove all the bindings to a disconnected DOM element. i.e.:isConnected
property isfalse
. - Pre-rendering recycling: before VanJS re-render the DOM tree in response to state changes, it will first check all the states whose values have been changed in this render cycle, and remove all the bindings to a disconnected DOM element.
Avoid your bindings to be GC-ed unexpectedly
There are some general guidelines to follow to avoid your bindings being garbage collected unexpectedly:
- Please complete the construction of the DOM tree and connect the newly constructed DOM tree to the
document
object before making any state changes. Otherwise, the bindings to yet-to-be-connected DOM elements will be garbage collected. - DOM tree construction needs to be synchronous. i.e.: you shouldn't have any suspension point while building the DOM tree (e.g.:
await
something in anasync function
). Otherwise, periodic recycling might be scheduled in the middle of the suspension point which can cause bindings to yet-to-be-connected DOM elements being garbage collected.
onnew
listeners are not subject to GC
Note that, the garbage collection in VanJS only removes obsolete bindings. It doesn't apply to event handlers registered via onnew
method. For instance, the code below still suffers from memory leaks:
const renderPre = van.state(false)
const text = van.state("Text")
const TextLine = (renderPre: boolean) => {
const expandedText = van.state("--Text--")
text.onnew(t => expandedText.val = `--${t}--`)
return (renderPre ? pre : div)(expandedText)
}
const dom = div(van.bind(renderPre, TextLine))
In this example, whenever the generation function TextLine
is called, a new State
object will be created and subscribe to the change of the text
state. Because every event handler registered via onnew
holds the reference to local State
variable expandedText
, the instances of expandedText
variable will not be GC-ed by JavaScript runtime even when they are no longer being actively used.
To avoid memory leaks caused by onnew
, in the generation function of the van.bind
call, you shall NEVER register event handlers via onnew
method for State
objects defined outside the scope of generation function.