dioxus/packages/core
2023-01-02 11:54:44 -06:00
..
.vscode fix: messed up how lifetimes worked, need to render once per component 2021-10-29 21:43:21 -04:00
src remove some commented out code 2023-01-02 11:54:44 -06:00
tests Merge branch 'upstream' into fix-non-str-attributes 2022-12-28 20:36:57 -06:00
architecture.md feat: use walking pattern 2022-10-26 18:04:47 -07:00
Cargo.toml Merge branch 'upstream' into fix-non-str-attributes 2022-12-30 11:52:55 -06:00
CHANGELOG.md feat: add changelogs 2022-01-29 10:17:14 -05:00
README.md feat: core tests passing 2022-12-05 14:16:54 -08:00

dioxus-core

dioxus-core is a fast and featureful VirtualDom implementation written in and for Rust.

Features

  • Functions as components
  • Hooks for local state
  • Task pool for spawning futures
  • Template-based architecture
  • Asynchronous components
  • Suspense boundaries
  • Error boundaries through the anyhow crate
  • Customizable memoization

If just starting out, check out the Guides first.

General Theory

The dioxus-core VirtualDom object is built around the concept of a Template. Templates describe a layout tree known at compile time with dynamic parts filled at runtime.

Each component in the VirtualDom works as a dedicated render loop where re-renders are triggered by events external to the VirtualDom, or from the components themselves.

When each component re-renders, it must return an Element. In Dioxus, the Element type is an alias for Result<VNode>. Between two renders, Dioxus compares the inner VNode object, and calculates the differences of the dynamic portions of each internal Template. If any attributes or elements are different between the old layout and new layout, Dioxus will write modifications to the Mutations object.

Dioxus expects the target renderer to save its nodes in a list. Each element is given a numerical ID which can be used to directly index into that list for O(1) lookups.

Usage

All Dioxus apps start as just a function that takes the [Scope] object and returns an [Element].

The dioxus crate exports the rsx macro which transforms a helpful, simpler syntax of Rust into the logic required to build Templates.

First, start with your app:

fn app(cx: Scope) -> Element {
    cx.render(rsx!( div { "hello world" } ))
}

Then, we'll want to create a new VirtualDom using this app as the root component.

let mut dom = VirtualDom::new(app);

To build the app into a stream of mutations, we'll use [VirtualDom::rebuild]:

let mutations = dom.rebuild();

apply_edits_to_real_dom(mutations);

We can then wait for any asynchronous components or pending futures using the wait_for_work() method. If we have a deadline, then we can use render_with_deadline instead:

// Wait for the dom to be marked dirty internally
dom.wait_for_work().await;

// Or wait for a deadline and then collect edits
dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)));

If an event occurs from outside the virtualdom while waiting for work, then we can cancel the wait using a select! block and inject the event.

loop {
    select! {
        evt = real_dom.event() => dom.handle_event("click", evt.data, evt.element, evt.bubbles),
        _ = dom.wait_for_work() => {}
    }

    // Render any work without blocking the main thread for too long
    let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)));

    // And then apply the edits
    real_dom.apply(mutations);
}

Internals

Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus borrows these concepts:

  • React: hooks, concurrency, suspense
  • Dodrio: bump allocation, double buffering, and some diffing architecture

Dioxus-core leverages some really cool techniques and hits a very high level of parity with mature frameworks. However, Dioxus also brings some new unique features:

  • managed lifetimes for borrowed data
  • placeholder approach for suspended vnodes
  • fiber/interruptible diffing algorithm
  • custom memory allocator for vnodes and all text content
  • support for fragments w/ lazy normalization
  • slab allocator for scopes
  • mirrored-slab approach for remote vdoms
  • dedicated subtrees for rendering into separate contexts from the same app

There's certainly more to the story, but these optimizations make Dioxus memory use and allocation count extremely minimal. For an average application, it is possible that zero allocations will need to be performed once the app has been loaded. Only when new components are added to the dom will allocations occur. For a given component, the space of old VNodes is dynamically recycled as new nodes are added. Additionally, Dioxus tracks the average memory footprint of previous components to estimate how much memory allocate for future components.

All in all, Dioxus treats memory as a valuable resource. Combined with the memory-efficient footprint of Wasm compilation, Dioxus apps can scale to thousands of components and still stay snappy.

Goals

The final implementation of Dioxus must:

  • Be fast. Allocators are typically slow in Wasm/Rust, so we should have a smart way of allocating.
  • Be memory efficient. Servers should handle tens of thousands of simultaneous VDoms with no problem.
  • Be concurrent. Components should be able to pause rendering to let the screen paint the next frame.
  • Be disconnected from a specific renderer (no WebSys dependency in the core crate).
  • Support server-side-rendering (SSR). VNodes should render to a string that can be served via a web server.
  • Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs.
  • Be modular. Components and hooks should be work anywhere without worrying about target platform.