Miso’s external API has three main parts. The model
, view
function, and update
function.
Under the hood miso’s concurrency model centers around an atomically updated IORef (Seq action)
(known henceforth as event queue). The queue is used to capture events and update the user-defined model
1. Each captured browser event appends an action
to the event queue.
miso
operates in an event loop that blocks on requestAnimationFrame. The model
lives on the stack of the event loop function. Inside this loop we drain the queue into [action]
, and fold against the model
(this is known as “event batching”). We do this to minimize calls to diff
.
Miso then diffs the new model
against the old model
. If the model
has been updated (dirty), the view
function is invoked on the updated model
produce a VTree
2. This VTree
gets converted to a JS
vtree
and is diffed + patched against the existing tree (that lives on the JS heap) and the DOM updates. The loop recurses on the new model
.
During diffing we mutually recurse over the parent and child nodes of both the old and new virtual DOM structures. During this process we diff the old tree against the new tree (while simultaneously updating the DOM to reflect the new structure) 3. We apply various new optimizations not seen in other frameworks (like react) on the child lists (see syncChildren). During diffing we also invoke node creation / destruction life cycle hooks.
While the event loop is executing, browser events are raised asynchronously and delegated into Haskell callbacks that live on the virtual DOM in the JS heap 4. The event body is parsed into a Haskell structure via JSON (FromJSON
), the update function is invoked to produce an Action
that gets written to the queue atomically. Event delegation and DOM diffing occur simultaneously.
Sub
and Sink
are ways to write into the actions queue externally (useful for integration with third party components as well). Each Component
has its own Sink
, and list of Sub
. There are some predefined Sub
in Miso.Subscription
for conveniently working with the History, Websocket, Keyboard and Mouse APIs. All Sub
are forked, and all Sink
writes are executed synchronously. A Sink
write appends to the event queue, its action
is evaluated asynchronously in the event loop thread.
Pre-rendering (using the miso
function) on application startup will traverse the DOM and copy pointers into miso’s virtual DOM structure (this process is known as hydration). This is necessary for events to work, since event delegation works by DOM pointer traversal on the virtual DOM to find the correct node to dispatch the event 4.
The view
function is how templating works in miso
. The View
is a Rose Tree that represents the DOM. This function is used in the event loop to construct new virtual DOMs in response to browser events.
Footnotes
-
Seq
is used to aid event ordering and avoid excessive redraws. ↩ -
Since events can be no-ops we want to avoid generating a tree if the model hasn’t changed. Miso uses both
Eq
andStablePtr
equality to determine if a draw is necessary. TheStablePtr
equality is an optimization that avoids expensive calls to(==)
on large models. ↩ -
This diff + patch approach is responsible for a lot of performance gains. We don't generate a list of patches and apply them in a separate phase like some other frameworks. ↩
-
The event handlers that live on the JS virtual DOM are how miso calls back into the Haskell heap from the JS heap to write to the
Action
queue. Events are defined on theView
using theonWithOptions
function. Lifecycle are also defined using the View DSL. ↩ ↩2