9.7 KiB
9.7 KiB
Dioxus Core - VirtualDOM Architecture
The dioxus-core crate is the heart of Dioxus, implementing the virtual DOM, component lifecycle, rendering pipeline, and reactive runtime.
VirtualDom Structure
The VirtualDom struct orchestrates the entire reactive component tree:
VirtualDom
├── scopes: Slab<ScopeState> // Arena allocator for all component scopes
├── dirty_scopes: BTreeSet<ScopeOrder> // Scopes marked for re-render (height-ordered)
├── runtime: Rc<Runtime> // Shared async runtime
├── resolved_scopes: Vec<ScopeId> // Scopes resolved during suspense
└── rx: UnboundedReceiver<SchedulerMsg> // Event channel from scheduler
Key Methods
VirtualDom::new(app)- Create new DOM with root componentrebuild()/rebuild_in_place()- Full tree reconstructionrender_immediate()- Render all dirty scopes without suspense blockingwait_for_work()- Async poll for futures and scheduler eventswait_for_suspense()- Block until all suspended futures completemark_dirty(scope_id)- Schedule scope for re-renderwith_root_context<T>()- Inject dependency into root scope
Rendering Pipeline
Initial Render Flow
rebuild()callsrun_scope(ScopeId::ROOT)- Result wraps in
LastRenderedNodeand callscreate_scope() create_scope()recursively creates all child scopes and DOM elements- Mutations written to
WriteMutationssink
Update Render Flow
wait_for_work()polls scheduler and pending futuresprocess_events()consumesSchedulerMsg::Immediate(scope_id)messagesrender_immediate()pops dirty scopes in height order (top-to-bottom)- For each scope:
run_and_diff_scope()executes component, thendiff_scope() - Diff generates mutations via
WriteMutationstrait
Component System
Component Definition
Component<P>type alias:fn(P) -> ElementComponentFunction<P, M>trait for component functionsfn_ptr(&self) -> usize- Raw function pointer for identityrebuild(&self, props: P) -> Element- Execute component
Props System
Propertiestrait for all props:type Builder- For DSL prop constructionbuilder()- Create props buildermemoize(&mut self, other: &Self) -> bool- Check if props changedinto_vcomponent()- Convert to VComponent
Type Erasure
VProps<F,P,M> wraps strongly-typed props, erased to BoxedAnyProps via AnyProps trait:
render(&self) -> Elementmemoize(&mut self, other: &dyn Any) -> boolprops()/props_mut()- Access asdyn Anyduplicate()- Clone into new box
Scope System
ScopeId
Small unique identifier (usize index into Slab):
ScopeId::ROOT(0) - Root wrapper scopeScopeId::ROOT_SUSPENSE_BOUNDARY(1) - Default suspenseScopeId::ROOT_ERROR_BOUNDARY(2) - Default error boundaryScopeId::APP(3) - User's root component
ScopeState (Public API)
ScopeState
├── context_id: ScopeId
├── last_rendered_node: Option<LastRenderedNode>
├── props: BoxedAnyProps
└── reactive_context: ReactiveContext
Scope (Internal State)
Scope
├── name: &'static str // Component name for debugging
├── id: ScopeId
├── parent_id: Option<ScopeId>
├── height: u32 // Distance from root
├── hooks: RefCell<Vec<Box<dyn Any>>> // Hook state storage
├── hook_index: Cell<usize> // Current hook being accessed
├── shared_contexts: RefCell<Vec<Box<dyn Any>>>
├── spawned_tasks: RefCell<FxHashSet<Task>>
├── before_render / after_render // Callbacks
├── status: RefCell<ScopeStatus> // Mounted/Unmounted
└── suspense_boundary: SuspenseLocation
Context Propagation
provide_context<T: Clone + 'static>(context: T)- Store in scopeconsume_context<T>()- Retrieve from this scope or any parent- Lookup walks parent chain via
parent_id
Event System
Event Structure
pub struct Event<T: ?Sized> {
pub data: Rc<T>,
pub(crate) metadata: Rc<RefCell<EventMetadata>>,
}
pub struct EventMetadata {
pub propagates: bool,
pub prevent_default: bool,
}
Event Flow
Runtime::handle_event()looks up element byElementId- Gets parent
ElementReffrom slab - If bubbles:
handle_bubbling_event()walks parent chain - Collects listeners in path order, calls in reverse (parents first)
- Breaks on
stop_propagation()
Diffing Algorithm
Entry Point
diff_scope() gets old/new VNodes and calls old.diff_node(new, dom, mutations)
VNode Diffing
- Template Check: Different templates → replace entire subtree
- Identity Check: Same pointer → skip
- Attribute Diffing:
diff_attributes()for dynamic attrs - Dynamic Node Diffing: Recursively diff each dynamic node
Dynamic Node Cases
- Text → Text:
diff_vtext()updates content - Placeholder → Placeholder: No-op
- Fragment → Fragment: Recursively diff children
- Component → Component: Check render fn, then memoize props
- Type Change: Remove old, create new
Keyed List Diffing
- Prefix Pass: Match keys from start
- Suffix Pass: Match keys from end
- Middle Pass: Handle insertions/deletions/moves
- Uses
FxHashMapfor old→new key matching
Mutations System
WriteMutations Trait
Interface between VirtualDOM and real DOM:
append_children(id, count)- Add N nodes to elementassign_node_id(path, id)- Mark element at template pathcreate_placeholder(id)- Create marker nodecreate_text_node(value, id)- Create text nodeload_template(template, index, id)- Clone from template cachereplace_node_with(id, count)- Replace elementset_attribute(name, ns, value, id)- Update attributecreate_event_listener(name, id)- Register listenerremove_node(id)- Delete element
Mutation Enum (for testing/serialization)
pub enum Mutation {
AppendChildren { id, m },
CreatePlaceholder { id },
CreateTextNode { value, id },
LoadTemplate { index, id },
ReplaceWith { id, m },
SetAttribute { name, ns, value, id },
// ... etc
}
Scheduler
Work Priority
- Dirty Scopes (Highest): Re-render in height order
- Tasks: Poll spawned futures
- Effects (Lowest): Run after DOM changes
ScopeOrder
Composite key: (height: u32, id: ScopeId) in BTreeSet ensures parent scopes run before children.
Key Methods
queue_scope(order)- Add to dirty_scopesqueue_task(task, order)- Add to dirty_taskspop_work()- Get next dirty scope or taskpop_effect()- Get next pending effect
Runtime
Manages async/scope/task coordination:
Runtime
├── scope_states: RefCell<Vec<Option<Scope>>>
├── scope_stack: RefCell<Vec<ScopeId>>
├── suspense_stack: RefCell<Vec<SuspenseLocation>>
├── tasks: RefCell<SlotMap<DefaultKey, Rc<LocalTask>>>
├── current_task: Cell<Option<Task>>
├── dirty_tasks: RefCell<BTreeSet<DirtyTasks>>
├── pending_effects: RefCell<BTreeSet<Effect>>
├── rendering: Cell<bool>
├── sender: UnboundedSender<SchedulerMsg>
├── elements: RefCell<Slab<Option<ElementRef>>>
└── mounts: RefCell<Slab<VNodeMount>>
Tasks & Async
Task Structure
id: TaskId(slotmap key)!Send + !Syncmarker
Task Methods
Task::new(future)- Spawn new taskcancel()- Remove taskpause()/resume()- Control pollingwake()- Wake sleeping task
LocalTask (Internal)
- Wraps
Pin<Box<dyn Future<Output = ()>>> - Custom waker that sends
SchedulerMsg::TaskNotified - Task dropped when owning scope drops
Effects
Effects run AFTER mutations are applied to DOM:
- Created via
use_effect()in component - Queued in
Runtime::pending_effects - After render,
finish_render()sendsSchedulerMsg::EffectQueued poll_tasks()pops effects and callseffect.run()
Suspense
SuspenseContext
- Tracks suspended futures and placeholder nodes
suspended_tasks: RefCell<Vec<SuspendedFuture>>suspended_nodes: RefCell<Option<VNode>>frozen: Cell<bool>for server-side locking
Suspension Flow
- Component calls
suspend()→Err(SuspendedFuture) Element::Err(RenderError::Suspended)propagates up- Nearest
SuspenseBoundarycatches it - Boundary renders placeholder
- Future added to boundary's tasks
- Boundary marked dirty when future completes
VNodes & Templates
VNode Structure
pub struct VNode {
vnode: Rc<VNodeInner>,
mount: Cell<MountId>,
}
pub struct VNodeInner {
pub key: Option<String>,
pub template: Template,
pub dynamic_nodes: Box<[DynamicNode]>,
pub dynamic_attrs: Box<[Box<[Attribute]>]>,
}
Template (Static)
pub struct Template {
pub roots: &'static [TemplateNode],
pub node_paths: &'static [&'static [u8]],
pub attr_paths: &'static [&'static [u8]],
}
TemplateNode Variants
Element { tag, namespace, attrs, children }- Static elementText { text }- Static textDynamic { id }- Index into dynamic_nodes
DynamicNode Variants
Component(VComponent)- Child componentText(VText)- Text nodePlaceholder(VPlaceholder)- Suspense/empty markerFragment(Vec<VNode>)- Multiple children
Error Boundaries
ErrorContext
error: Rc<RefCell<Option<CapturedError>>>subscribers: Subscribersfor listening components
Error Flow
- Component returns
Element::Err(error) - Nearest
ErrorBoundarycatches ErrorContextstores error- Boundary re-renders with
error_context.error()available
Root Wrapper
Default scope hierarchy:
ScopeId(0): RootScopeWrapper
└── ScopeId(1): SuspenseBoundary
└── ScopeId(2): ErrorBoundary
└── ScopeId(3): User's Root App
Provides default suspense and error handling for the entire tree.