123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669 |
- //! # Virtual DOM Implementation for Rust
- //!
- //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
- use crate::{
- any_props::VProps,
- arena::{ElementId, ElementRef},
- innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
- mutations::Mutation,
- nodes::RenderReturn,
- nodes::{Template, TemplateId},
- runtime::{Runtime, RuntimeGuard},
- scopes::{ScopeId, ScopeState},
- AttributeValue, Element, Event, Scope,
- };
- use futures_util::{pin_mut, StreamExt};
- use rustc_hash::{FxHashMap, FxHashSet};
- use slab::Slab;
- use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
- /// A virtual node system that progresses user events and diffs UI trees.
- ///
- /// ## Guide
- ///
- /// Components are defined as simple functions that take [`Scope`] and return an [`Element`].
- ///
- /// ```rust
- /// # use dioxus::prelude::*;
- ///
- /// #[derive(Props, PartialEq)]
- /// struct AppProps {
- /// title: String
- /// }
- ///
- /// fn App(cx: Scope<AppProps>) -> Element {
- /// cx.render(rsx!(
- /// div {"hello, {cx.props.title}"}
- /// ))
- /// }
- /// ```
- ///
- /// Components may be composed to make complex apps.
- ///
- /// ```rust
- /// # #![allow(unused)]
- /// # use dioxus::prelude::*;
- ///
- /// # #[derive(Props, PartialEq)]
- /// # struct AppProps {
- /// # title: String
- /// # }
- ///
- /// static ROUTES: &str = "";
- ///
- /// fn App(cx: Scope<AppProps>) -> Element {
- /// cx.render(rsx!(
- /// NavBar { routes: ROUTES }
- /// Title { "{cx.props.title}" }
- /// Footer {}
- /// ))
- /// }
- ///
- /// #[inline_props]
- /// fn NavBar(cx: Scope, routes: &'static str) -> Element {
- /// cx.render(rsx! {
- /// div { "Routes: {routes}" }
- /// })
- /// }
- ///
- /// fn Footer(cx: Scope) -> Element {
- /// cx.render(rsx! { div { "Footer" } })
- /// }
- ///
- /// #[inline_props]
- /// fn Title<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {
- /// cx.render(rsx! {
- /// div { id: "title", children }
- /// })
- /// }
- /// ```
- ///
- /// To start an app, create a [`VirtualDom`] and call [`VirtualDom::rebuild`] to get the list of edits required to
- /// draw the UI.
- ///
- /// ```rust
- /// # use dioxus::prelude::*;
- /// # fn App(cx: Scope) -> Element { cx.render(rsx! { div {} }) }
- ///
- /// let mut vdom = VirtualDom::new(App);
- /// let edits = vdom.rebuild();
- /// ```
- ///
- /// To call listeners inside the VirtualDom, call [`VirtualDom::handle_event`] with the appropriate event data.
- ///
- /// ```rust, ignore
- /// vdom.handle_event(event);
- /// ```
- ///
- /// While no events are ready, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom.
- ///
- /// ```rust, ignore
- /// vdom.wait_for_work().await;
- /// ```
- ///
- /// Once work is ready, call [`VirtualDom::render_with_deadline`] to compute the differences between the previous and
- /// current UI trees. This will return a [`Mutations`] object that contains Edits, Effects, and NodeRefs that need to be
- /// handled by the renderer.
- ///
- /// ```rust, ignore
- /// let mutations = vdom.work_with_deadline(tokio::time::sleep(Duration::from_millis(100)));
- ///
- /// for edit in mutations.edits {
- /// real_dom.apply(edit);
- /// }
- /// ```
- ///
- /// To not wait for suspense while diffing the VirtualDom, call [`VirtualDom::render_immediate`] or pass an immediately
- /// ready future to [`VirtualDom::render_with_deadline`].
- ///
- ///
- /// ## Building an event loop around Dioxus:
- ///
- /// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above.
- /// ```rust, ignore
- /// fn app(cx: Scope) -> Element {
- /// cx.render(rsx! {
- /// div { "Hello World" }
- /// })
- /// }
- ///
- /// let dom = VirtualDom::new(app);
- ///
- /// real_dom.apply(dom.rebuild());
- ///
- /// loop {
- /// select! {
- /// _ = dom.wait_for_work() => {}
- /// evt = real_dom.wait_for_event() => dom.handle_event(evt),
- /// }
- ///
- /// real_dom.apply(dom.render_immediate());
- /// }
- /// ```
- ///
- /// ## Waiting for suspense
- ///
- /// Because Dioxus supports suspense, you can use it for server-side rendering, static site generation, and other usecases
- /// where waiting on portions of the UI to finish rendering is important. To wait for suspense, use the
- /// [`VirtualDom::render_with_deadline`] method:
- ///
- /// ```rust, ignore
- /// let dom = VirtualDom::new(app);
- ///
- /// let deadline = tokio::time::sleep(Duration::from_millis(100));
- /// let edits = dom.render_with_deadline(deadline).await;
- /// ```
- ///
- /// ## Use with streaming
- ///
- /// If not all rendering is done by the deadline, it might be worthwhile to stream the rest later. To do this, we
- /// suggest rendering with a deadline, and then looping between [`VirtualDom::wait_for_work`] and render_immediate until
- /// no suspended work is left.
- ///
- /// ```rust, ignore
- /// let dom = VirtualDom::new(app);
- ///
- /// let deadline = tokio::time::sleep(Duration::from_millis(20));
- /// let edits = dom.render_with_deadline(deadline).await;
- ///
- /// real_dom.apply(edits);
- ///
- /// while dom.has_suspended_work() {
- /// dom.wait_for_work().await;
- /// real_dom.apply(dom.render_immediate());
- /// }
- /// ```
- pub struct VirtualDom {
- pub(crate) scopes: Slab<Box<ScopeState>>,
- pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
- // Maps a template path to a map of byteindexes to templates
- pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
- // Every element is actually a dual reference - one to the template and the other to the dynamic node in that template
- pub(crate) elements: Slab<ElementRef>,
- pub(crate) mutations: Mutations<'static>,
- pub(crate) runtime: Rc<Runtime>,
- // Currently suspended scopes
- pub(crate) suspended_scopes: FxHashSet<ScopeId>,
- pub(crate) rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
- }
- impl VirtualDom {
- /// Create a new VirtualDom with a component that does not have special props.
- ///
- /// # Description
- ///
- /// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
- ///
- /// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
- /// to toss out the entire tree.
- ///
- ///
- /// # Example
- /// ```rust, ignore
- /// fn Example(cx: Scope) -> Element {
- /// cx.render(rsx!( div { "hello world" } ))
- /// }
- ///
- /// let dom = VirtualDom::new(Example);
- /// ```
- ///
- /// Note: the VirtualDom is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it.
- pub fn new(app: fn(Scope) -> Element) -> Self {
- Self::new_with_props(app, ())
- }
- /// Create a new VirtualDom with the given properties for the root component.
- ///
- /// # Description
- ///
- /// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
- ///
- /// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
- /// to toss out the entire tree.
- ///
- ///
- /// # Example
- /// ```rust, ignore
- /// #[derive(PartialEq, Props)]
- /// struct SomeProps {
- /// name: &'static str
- /// }
- ///
- /// fn Example(cx: Scope<SomeProps>) -> Element {
- /// cx.render(rsx!{ div{ "hello {cx.props.name}" } })
- /// }
- ///
- /// let dom = VirtualDom::new(Example);
- /// ```
- ///
- /// Note: the VirtualDom is not progressed on creation. You must either "run_with_deadline" or use "rebuild" to progress it.
- ///
- /// ```rust, ignore
- /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
- /// let mutations = dom.rebuild();
- /// ```
- pub fn new_with_props<P: 'static>(root: fn(Scope<P>) -> Element, root_props: P) -> Self {
- let (tx, rx) = futures_channel::mpsc::unbounded();
- let scheduler = Scheduler::new(tx);
- let mut dom = Self {
- rx,
- runtime: Runtime::new(scheduler),
- scopes: Default::default(),
- dirty_scopes: Default::default(),
- templates: Default::default(),
- elements: Default::default(),
- mutations: Mutations::default(),
- suspended_scopes: Default::default(),
- };
- let root = dom.new_scope(
- Box::new(VProps::new(root, |_, _| unreachable!(), root_props)),
- "app",
- );
- // Unlike react, we provide a default error boundary that just renders the error as a string
- root.provide_context(Rc::new(ErrorBoundary::new(ScopeId::ROOT)));
- // the root element is always given element ID 0 since it's the container for the entire tree
- dom.elements.insert(ElementRef::none());
- dom
- }
- /// Get the state for any scope given its ID
- ///
- /// This is useful for inserting or removing contexts from a scope, or rendering out its root node
- pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
- self.scopes.get(id.0).map(|s| &**s)
- }
- /// Get the single scope at the top of the VirtualDom tree that will always be around
- ///
- /// This scope has a ScopeId of 0 and is the root of the tree
- pub fn base_scope(&self) -> &ScopeState {
- self.get_scope(ScopeId::ROOT).unwrap()
- }
- /// Build the virtualdom with a global context inserted into the base scope
- ///
- /// This is useful for what is essentially dependency injection when building the app
- pub fn with_root_context<T: Clone + 'static>(self, context: T) -> Self {
- self.base_scope().provide_context(context);
- self
- }
- /// Manually mark a scope as requiring a re-render
- ///
- /// Whenever the Runtime "works", it will re-render this scope
- pub fn mark_dirty(&mut self, id: ScopeId) {
- if let Some(scope) = self.get_scope(id) {
- let height = scope.height();
- tracing::trace!("Marking scope {:?} ({}) as dirty", id, scope.context().name);
- self.dirty_scopes.insert(DirtyScope { height, id });
- }
- }
- /// Call a listener inside the VirtualDom with data from outside the VirtualDom.
- ///
- /// This method will identify the appropriate element. The data must match up with the listener delcared. Note that
- /// this method does not give any indication as to the success of the listener call. If the listener is not found,
- /// nothing will happen.
- ///
- /// It is up to the listeners themselves to mark nodes as dirty.
- ///
- /// If you have multiple events, you can call this method multiple times before calling "render_with_deadline"
- pub fn handle_event(
- &mut self,
- name: &str,
- data: Rc<dyn Any>,
- element: ElementId,
- bubbles: bool,
- ) {
- let _runtime = RuntimeGuard::new(self.runtime.clone());
- /*
- ------------------------
- The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when
- we find the target path.
- With the target path, we try and move up to the parent until there is no parent.
- Due to how bubbling works, we call the listeners before walking to the parent.
- If we wanted to do capturing, then we would accumulate all the listeners and call them in reverse order.
- ----------------------
- For a visual demonstration, here we present a tree on the left and whether or not a listener is collected on the
- right.
- | <-- yes (is ascendant)
- | | | <-- no (is not direct ascendant)
- | | <-- yes (is ascendant)
- | | | | | <--- target element, break early, don't check other listeners
- | | | <-- no, broke early
- | <-- no, broke early
- */
- let mut parent_path = self.elements.get(element.0);
- let mut listeners = vec![];
- // We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
- let uievent = Event {
- propagates: Rc::new(Cell::new(bubbles)),
- data,
- };
- // If the event bubbles, we traverse through the tree until we find the target element.
- if bubbles {
- // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
- while let Some(el_ref) = parent_path {
- // safety: we maintain references of all vnodes in the element slab
- if let Some(template) = el_ref.template {
- let template = unsafe { template.as_ref() };
- let node_template = template.template.get();
- let target_path = el_ref.path;
- for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
- let this_path = node_template.attr_paths[idx];
- // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
- if attr.name.trim_start_matches("on") == name
- && target_path.is_decendant(&this_path)
- {
- listeners.push(&attr.value);
- // Break if this is the exact target element.
- // This means we won't call two listeners with the same name on the same element. This should be
- // documented, or be rejected from the rsx! macro outright
- if target_path == this_path {
- break;
- }
- }
- }
- // Now that we've accumulated all the parent attributes for the target element, call them in reverse order
- // We check the bubble state between each call to see if the event has been stopped from bubbling
- for listener in listeners.drain(..).rev() {
- if let AttributeValue::Listener(listener) = listener {
- let origin = el_ref.scope;
- self.runtime.scope_stack.borrow_mut().push(origin);
- self.runtime.rendering.set(false);
- if let Some(cb) = listener.borrow_mut().as_deref_mut() {
- cb(uievent.clone());
- }
- self.runtime.scope_stack.borrow_mut().pop();
- self.runtime.rendering.set(true);
- if !uievent.propagates.get() {
- return;
- }
- }
- }
- parent_path = template.parent.and_then(|id| self.elements.get(id.0));
- } else {
- break;
- }
- }
- } else {
- // Otherwise, we just call the listener on the target element
- if let Some(el_ref) = parent_path {
- // safety: we maintain references of all vnodes in the element slab
- if let Some(template) = el_ref.template {
- let template = unsafe { template.as_ref() };
- let node_template = template.template.get();
- let target_path = el_ref.path;
- for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
- let this_path = node_template.attr_paths[idx];
- // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
- // Only call the listener if this is the exact target element.
- if attr.name.trim_start_matches("on") == name && target_path == this_path {
- if let AttributeValue::Listener(listener) = &attr.value {
- let origin = el_ref.scope;
- self.runtime.scope_stack.borrow_mut().push(origin);
- self.runtime.rendering.set(false);
- if let Some(cb) = listener.borrow_mut().as_deref_mut() {
- cb(uievent.clone());
- }
- self.runtime.scope_stack.borrow_mut().pop();
- self.runtime.rendering.set(true);
- break;
- }
- }
- }
- }
- }
- }
- }
- /// Wait for the scheduler to have any work.
- ///
- /// This method polls the internal future queue, waiting for suspense nodes, tasks, or other work. This completes when
- /// any work is ready. If multiple scopes are marked dirty from a task or a suspense tree is finished, this method
- /// will exit.
- ///
- /// This method is cancel-safe, so you're fine to discard the future in a select block.
- ///
- /// This lets us poll async tasks and suspended trees during idle periods without blocking the main thread.
- ///
- /// # Example
- ///
- /// ```rust, ignore
- /// let dom = VirtualDom::new(App);
- /// let sender = dom.get_scheduler_channel();
- /// ```
- pub async fn wait_for_work(&mut self) {
- let mut some_msg = None;
- loop {
- match some_msg.take() {
- // If a bunch of messages are ready in a sequence, try to pop them off synchronously
- Some(msg) => match msg {
- SchedulerMsg::Immediate(id) => self.mark_dirty(id),
- SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
- },
- // If they're not ready, then we should wait for them to be ready
- None => {
- match self.rx.try_next() {
- Ok(Some(val)) => some_msg = Some(val),
- Ok(None) => return,
- Err(_) => {
- // If we have any dirty scopes, or finished fiber trees then we should exit
- if !self.dirty_scopes.is_empty() || !self.suspended_scopes.is_empty() {
- return;
- }
- some_msg = self.rx.next().await
- }
- }
- }
- }
- }
- }
- /// Process all events in the queue until there are no more left
- pub fn process_events(&mut self) {
- while let Ok(Some(msg)) = self.rx.try_next() {
- match msg {
- SchedulerMsg::Immediate(id) => self.mark_dirty(id),
- SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
- }
- }
- }
- /// Replace a template at runtime. This will re-render all components that use this template.
- /// This is the primitive that enables hot-reloading.
- ///
- /// The caller must ensure that the template refrences the same dynamic attributes and nodes as the original template.
- ///
- /// This will only replace the the parent template, not any nested templates.
- pub fn replace_template(&mut self, template: Template<'static>) {
- self.register_template_first_byte_index(template);
- // iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
- for (_, scope) in self.scopes.iter() {
- if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() {
- if sync.template.get().name.rsplit_once(':').unwrap().0
- == template.name.rsplit_once(':').unwrap().0
- {
- let context = scope.context();
- let height = context.height;
- self.dirty_scopes.insert(DirtyScope {
- height,
- id: context.id,
- });
- }
- }
- }
- }
- /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch.
- ///
- /// The mutations item expects the RealDom's stack to be the root of the application.
- ///
- /// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the
- /// root component will be ran once and then diffed. All updates will flow out as mutations.
- ///
- /// All state stored in components will be completely wiped away.
- ///
- /// Any templates previously registered will remain.
- ///
- /// # Example
- /// ```rust, ignore
- /// static App: Component = |cx| cx.render(rsx!{ "hello world" });
- ///
- /// let mut dom = VirtualDom::new();
- /// let edits = dom.rebuild();
- ///
- /// apply_edits(edits);
- /// ```
- pub fn rebuild(&mut self) -> Mutations {
- let _runtime = RuntimeGuard::new(self.runtime.clone());
- match unsafe { self.run_scope(ScopeId::ROOT).extend_lifetime_ref() } {
- // Rebuilding implies we append the created elements to the root
- RenderReturn::Ready(node) => {
- let m = self.create_scope(ScopeId::ROOT, node);
- self.mutations.edits.push(Mutation::AppendChildren {
- id: ElementId(0),
- m,
- });
- }
- // If an error occurs, we should try to render the default error component and context where the error occured
- RenderReturn::Aborted(placeholder) => {
- tracing::debug!("Ran into suspended or aborted scope during rebuild");
- let id = self.next_null();
- placeholder.id.set(Some(id));
- self.mutations.push(Mutation::CreatePlaceholder { id });
- }
- }
- self.finalize()
- }
- /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress
- /// suspended subtrees.
- pub fn render_immediate(&mut self) -> Mutations {
- // Build a waker that won't wake up since our deadline is already expired when it's polled
- let waker = futures_util::task::noop_waker();
- let mut cx = std::task::Context::from_waker(&waker);
- // Now run render with deadline but dont even try to poll any async tasks
- let fut = self.render_with_deadline(std::future::ready(()));
- pin_mut!(fut);
- // The root component is not allowed to be async
- match fut.poll(&mut cx) {
- std::task::Poll::Ready(mutations) => mutations,
- std::task::Poll::Pending => panic!("render_immediate should never return pending"),
- }
- }
- /// Render the virtual dom, waiting for all suspense to be finished
- ///
- /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
- pub async fn wait_for_suspense(&mut self) {
- loop {
- // println!("waiting for suspense {:?}", self.suspended_scopes);
- if self.suspended_scopes.is_empty() {
- return;
- }
- // println!("waiting for suspense");
- self.wait_for_work().await;
- // println!("Rendered immediately");
- _ = self.render_immediate();
- }
- }
- /// Render what you can given the timeline and then move on
- ///
- /// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues.
- ///
- /// If no suspense trees are present
- pub async fn render_with_deadline(&mut self, deadline: impl Future<Output = ()>) -> Mutations {
- pin_mut!(deadline);
- self.process_events();
- loop {
- // Next, diff any dirty scopes
- // We choose not to poll the deadline since we complete pretty quickly anyways
- if let Some(dirty) = self.dirty_scopes.iter().next().cloned() {
- self.dirty_scopes.remove(&dirty);
- // If the scope doesn't exist for whatever reason, then we should skip it
- if !self.scopes.contains(dirty.id.0) {
- continue;
- }
- {
- let _runtime = RuntimeGuard::new(self.runtime.clone());
- // Run the scope and get the mutations
- self.run_scope(dirty.id);
- self.diff_scope(dirty.id);
- }
- }
- // If there's more work, then just continue, plenty of work to do
- if !self.dirty_scopes.is_empty() {
- continue;
- }
- // Poll the suspense leaves in the meantime
- let mut work = self.wait_for_work();
- // safety: this is okay since we don't touch the original future
- let pinned = unsafe { std::pin::Pin::new_unchecked(&mut work) };
- // If the deadline is exceded (left) then we should return the mutations we have
- use futures_util::future::{select, Either};
- if let Either::Left((_, _)) = select(&mut deadline, pinned).await {
- // release the borrowed
- drop(work);
- return self.finalize();
- }
- }
- }
- /// Swap the current mutations with a new
- fn finalize(&mut self) -> Mutations {
- std::mem::take(&mut self.mutations)
- }
- }
- impl Drop for VirtualDom {
- fn drop(&mut self) {
- // Simply drop this scope which drops all of its children
- self.drop_scope(ScopeId::ROOT, true);
- }
- }
|