123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917 |
- //! # Virtual DOM Implementation for Rust
- //!
- //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
- use crate::innerlude::Work;
- use crate::properties::RootProps;
- use crate::root_wrapper::RootScopeWrapper;
- use crate::{
- arena::ElementId,
- innerlude::{
- ElementRef, NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState, VNodeMount, VProps,
- WriteMutations,
- },
- runtime::{Runtime, RuntimeGuard},
- scopes::ScopeId,
- AttributeValue, ComponentFunction, Element, Event, Mutations,
- };
- use crate::{Task, VComponent};
- use futures_util::StreamExt;
- use slab::Slab;
- use std::collections::BTreeSet;
- use std::{any::Any, rc::Rc};
- use tracing::instrument;
- /// A virtual node system that progresses user events and diffs UI trees.
- ///
- /// ## Guide
- ///
- /// Components are defined as simple functions that take [`crate::properties::Properties`] and return an [`Element`].
- ///
- /// ```rust
- /// # use dioxus::prelude::*;
- ///
- /// #[derive(Props, PartialEq, Clone)]
- /// struct AppProps {
- /// title: String
- /// }
- ///
- /// fn app(cx: AppProps) -> Element {
- /// rsx!(
- /// div {"hello, {cx.title}"}
- /// )
- /// }
- /// ```
- ///
- /// Components may be composed to make complex apps.
- ///
- /// ```rust
- /// # #![allow(unused)]
- /// # use dioxus::prelude::*;
- ///
- /// # #[derive(Props, PartialEq, Clone)]
- /// # struct AppProps {
- /// # title: String
- /// # }
- ///
- /// static ROUTES: &str = "";
- ///
- /// #[component]
- /// fn app(cx: AppProps) -> Element {
- /// rsx!(
- /// NavBar { routes: ROUTES }
- /// Title { "{cx.title}" }
- /// Footer {}
- /// )
- /// }
- ///
- /// #[component]
- /// fn NavBar( routes: &'static str) -> Element {
- /// rsx! {
- /// div { "Routes: {routes}" }
- /// }
- /// }
- ///
- /// #[component]
- /// fn Footer() -> Element {
- /// rsx! { div { "Footer" } }
- /// }
- ///
- /// #[component]
- /// fn Title( children: Element) -> Element {
- /// 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::*;
- /// # use dioxus_core::*;
- /// # fn app() -> Element { rsx! { div {} } }
- ///
- /// let mut vdom = VirtualDom::new(app);
- /// let edits = vdom.rebuild_to_vec();
- /// ```
- ///
- /// To call listeners inside the VirtualDom, call [`VirtualDom::handle_event`] with the appropriate event data.
- ///
- /// ```rust, no_run
- /// # use dioxus::prelude::*;
- /// # use dioxus_core::*;
- /// # fn app() -> Element { rsx! { div {} } }
- /// # let mut vdom = VirtualDom::new(app);
- /// let event = std::rc::Rc::new(0);
- /// vdom.handle_event("onclick", event, ElementId(0), true);
- /// ```
- ///
- /// While no events are ready, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom.
- ///
- /// ```rust, no_run
- /// # use dioxus::prelude::*;
- /// # use dioxus_core::*;
- /// # fn app() -> Element { rsx! { div {} } }
- /// # let mut vdom = VirtualDom::new(app);
- /// tokio::runtime::Runtime::new().unwrap().block_on(async {
- /// vdom.wait_for_work().await;
- /// });
- /// ```
- ///
- /// Once work is ready, call [`VirtualDom::render_immediate`] to compute the differences between the previous and
- /// current UI trees. This will write edits to a [`WriteMutations`] object you pass in that contains with edits that need to be
- /// handled by the renderer.
- ///
- /// ```rust, no_run
- /// # use dioxus::prelude::*;
- /// # use dioxus_core::*;
- /// # fn app() -> Element { rsx! { div {} } }
- /// # let mut vdom = VirtualDom::new(app);
- /// let mut mutations = Mutations::default();
- ///
- /// vdom.render_immediate(&mut mutations);
- /// ```
- ///
- /// To not wait for suspense while diffing the VirtualDom, call [`VirtualDom::render_immediate`].
- ///
- ///
- /// ## Building an event loop around Dioxus:
- ///
- /// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above.
- /// ```rust, no_run
- /// # use dioxus::prelude::*;
- /// # use dioxus_core::*;
- /// # struct RealDom;
- /// # struct Event {}
- /// # impl RealDom {
- /// # fn new() -> Self {
- /// # Self {}
- /// # }
- /// # fn apply(&mut self) -> Mutations {
- /// # unimplemented!()
- /// # }
- /// # async fn wait_for_event(&mut self) -> std::rc::Rc<dyn std::any::Any> {
- /// # unimplemented!()
- /// # }
- /// # }
- /// #
- /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
- /// let mut real_dom = RealDom::new();
- ///
- /// #[component]
- /// fn app() -> Element {
- /// rsx! {
- /// div { "Hello World" }
- /// }
- /// }
- ///
- /// let mut dom = VirtualDom::new(app);
- ///
- /// dom.rebuild(&mut real_dom.apply());
- ///
- /// loop {
- /// tokio::select! {
- /// _ = dom.wait_for_work() => {}
- /// evt = real_dom.wait_for_event() => dom.handle_event("onclick", evt, ElementId(0), true),
- /// }
- ///
- /// dom.render_immediate(&mut real_dom.apply());
- /// }
- /// # });
- /// ```
- ///
- /// ## Waiting for suspense
- ///
- /// Because Dioxus supports suspense, you can use it for server-side rendering, static site generation, and other use cases
- /// where waiting on portions of the UI to finish rendering is important. To wait for suspense, use the
- /// [`VirtualDom::wait_for_suspense`] method:
- ///
- /// ```rust, no_run
- /// # use dioxus::prelude::*;
- /// # use dioxus_core::*;
- /// # fn app() -> Element { rsx! { div {} } }
- /// tokio::runtime::Runtime::new().unwrap().block_on(async {
- /// let mut dom = VirtualDom::new(app);
- ///
- /// dom.rebuild_in_place();
- /// dom.wait_for_suspense().await;
- /// });
- ///
- /// // Render the virtual dom
- /// ```
- pub struct VirtualDom {
- pub(crate) scopes: Slab<ScopeState>,
- pub(crate) dirty_scopes: BTreeSet<ScopeOrder>,
- // The element ids that are used in the renderer
- // These mark a specific place in a whole rsx block
- pub(crate) elements: Slab<Option<ElementRef>>,
- // Once nodes are mounted, the information about where they are mounted is stored here
- // We need to store this information on the virtual dom so that we know what nodes are mounted where when we bubble events
- // Each mount is associated with a whole rsx block. [`VirtualDom::elements`] link to a specific node in the block
- pub(crate) mounts: Slab<VNodeMount>,
- pub(crate) runtime: Rc<Runtime>,
- // The scopes that have been resolved since the last render
- pub(crate) resolved_scopes: Vec<ScopeId>,
- 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, no_run
- /// # use dioxus::prelude::*;
- /// # use dioxus_core::*;
- /// fn Example() -> Element {
- /// 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() -> Element) -> Self {
- Self::new_with_props(
- move || {
- use warnings::Warning;
- // The root props don't come from a vcomponent so we need to manually rerun them sometimes
- crate::properties::component_called_as_function::allow(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, no_run
- /// # use dioxus::prelude::*;
- /// # use dioxus_core::*;
- /// #[derive(PartialEq, Props, Clone)]
- /// struct SomeProps {
- /// name: &'static str
- /// }
- ///
- /// fn Example(cx: SomeProps) -> Element {
- /// rsx!{ div { "hello {cx.name}" } }
- /// }
- ///
- /// let dom = VirtualDom::new_with_props(Example, SomeProps { name: "world" });
- /// ```
- ///
- /// Note: the VirtualDom is not progressed on creation. You must either "run_with_deadline" or use "rebuild" to progress it.
- ///
- /// ```rust, no_run
- /// # use dioxus::prelude::*;
- /// # use dioxus_core::*;
- /// # #[derive(PartialEq, Props, Clone)]
- /// # struct SomeProps {
- /// # name: &'static str
- /// # }
- /// # fn Example(cx: SomeProps) -> Element {
- /// # rsx!{ div { "hello {cx.name}" } }
- /// # }
- /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
- /// dom.rebuild_in_place();
- /// ```
- pub fn new_with_props<P: Clone + 'static, M: 'static>(
- root: impl ComponentFunction<P, M>,
- root_props: P,
- ) -> Self {
- let render_fn = root.id();
- let props = VProps::new(root, |_, _| true, root_props, "Root");
- Self::new_with_component(VComponent {
- name: "root",
- render_fn,
- props: Box::new(props),
- })
- }
- /// Create a new virtualdom and build it immediately
- pub fn prebuilt(app: fn() -> Element) -> Self {
- let mut dom = Self::new(app);
- dom.rebuild_in_place();
- dom
- }
- /// Create a new VirtualDom from something that implements [`AnyProps`]
- #[instrument(skip(root), level = "trace", name = "VirtualDom::new")]
- pub(crate) fn new_with_component(root: VComponent) -> Self {
- let (tx, rx) = futures_channel::mpsc::unbounded();
- let mut dom = Self {
- rx,
- runtime: Runtime::new(tx),
- scopes: Default::default(),
- dirty_scopes: Default::default(),
- elements: Default::default(),
- mounts: Default::default(),
- resolved_scopes: Default::default(),
- };
- let root = VProps::new(
- RootScopeWrapper,
- |_, _| true,
- RootProps(root),
- "RootWrapper",
- );
- dom.new_scope(Box::new(root), "app");
- // the root element is always given element ID 0 since it's the container for the entire tree
- dom.elements.insert(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)
- }
- /// 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()
- }
- /// Run a closure inside the dioxus runtime
- #[instrument(skip(self, f), level = "trace", name = "VirtualDom::in_runtime")]
- pub fn in_runtime<O>(&self, f: impl FnOnce() -> O) -> O {
- let _runtime = RuntimeGuard::new(self.runtime.clone());
- f()
- }
- /// 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().state().provide_context(context);
- self
- }
- /// Provide a context to the root scope
- pub fn provide_root_context<T: Clone + 'static>(&self, context: T) {
- self.base_scope().state().provide_context(context);
- }
- /// Build the virtualdom with a global context inserted into the base scope
- ///
- /// This method is useful for when you want to provide a context in your app without knowing its type
- pub fn insert_any_root_context(&mut self, context: Box<dyn Any>) {
- self.base_scope().state().provide_any_context(context);
- }
- /// 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) {
- let Some(scope) = self.runtime.get_state(id) else {
- return;
- };
- tracing::event!(tracing::Level::TRACE, "Marking scope {:?} as dirty", id);
- let order = ScopeOrder::new(scope.height(), id);
- drop(scope);
- self.queue_scope(order);
- }
- /// Mark a task as dirty
- fn mark_task_dirty(&mut self, task: Task) {
- let Some(scope) = self.runtime.task_scope(task) else {
- return;
- };
- let Some(scope) = self.runtime.get_state(scope) else {
- return;
- };
- tracing::event!(
- tracing::Level::TRACE,
- "Marking task {:?} (spawned in {:?}) as dirty",
- task,
- scope.id,
- );
- let order = ScopeOrder::new(scope.height(), scope.id);
- drop(scope);
- self.queue_task(task, order);
- }
- /// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an element with a listener, not a static node or a text node.**
- ///
- /// This method will identify the appropriate element. The data must match up with the listener declared. 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"
- #[instrument(skip(self), level = "trace", name = "VirtualDom::handle_event")]
- pub fn handle_event(
- &mut self,
- name: &str,
- data: Rc<dyn Any>,
- element: ElementId,
- bubbles: bool,
- ) {
- let _runtime = RuntimeGuard::new(self.runtime.clone());
- if let Some(Some(parent_path)) = self.elements.get(element.0).copied() {
- if bubbles {
- self.handle_bubbling_event(parent_path, name, Event::new(data, bubbles));
- } else {
- self.handle_non_bubbling_event(parent_path, name, Event::new(data, bubbles));
- }
- }
- }
- /// 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, no_run
- /// # use dioxus::prelude::*;
- /// # fn app() -> Element { rsx! { div {} } }
- /// let dom = VirtualDom::new(app);
- /// ```
- #[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_work")]
- pub async fn wait_for_work(&mut self) {
- loop {
- // Process all events - Scopes are marked dirty, etc
- // Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
- self.process_events();
- // Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
- if self.has_dirty_scopes() {
- return;
- }
- // Make sure we set the runtime since we're running user code
- let _runtime = RuntimeGuard::new(self.runtime.clone());
- // There isn't any more work we can do synchronously. Wait for any new work to be ready
- self.wait_for_event().await;
- }
- }
- /// Wait for the next event to trigger and add it to the queue
- #[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_event")]
- async fn wait_for_event(&mut self) {
- match self.rx.next().await.expect("channel should never close") {
- SchedulerMsg::Immediate(id) => self.mark_dirty(id),
- SchedulerMsg::TaskNotified(id) => {
- // Instead of running the task immediately, we insert it into the runtime's task queue.
- // The task may be marked dirty at the same time as the scope that owns the task is dropped.
- self.mark_task_dirty(Task::from_id(id));
- }
- SchedulerMsg::EffectQueued => {}
- };
- }
- /// Queue any pending events
- fn queue_events(&mut self) {
- // Prevent a task from deadlocking the runtime by repeatedly queueing itself
- while let Ok(Some(msg)) = self.rx.try_next() {
- match msg {
- SchedulerMsg::Immediate(id) => self.mark_dirty(id),
- SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)),
- SchedulerMsg::EffectQueued => {}
- }
- }
- }
- /// Process all events in the queue until there are no more left
- #[instrument(skip(self), level = "trace", name = "VirtualDom::process_events")]
- pub fn process_events(&mut self) {
- self.queue_events();
- // Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
- if self.has_dirty_scopes() {
- return;
- }
- self.poll_tasks()
- }
- /// Poll any queued tasks
- #[instrument(skip(self), level = "trace", name = "VirtualDom::poll_tasks")]
- fn poll_tasks(&mut self) {
- // Make sure we set the runtime since we're running user code
- let _runtime = RuntimeGuard::new(self.runtime.clone());
- // Keep polling tasks until there are no more effects or tasks to run
- // Or until we have no more dirty scopes
- while !self.runtime.dirty_tasks.borrow().is_empty()
- || !self.runtime.pending_effects.borrow().is_empty()
- {
- // Next, run any queued tasks
- // We choose not to poll the deadline since we complete pretty quickly anyways
- while let Some(task) = self.pop_task() {
- let _ = self.runtime.handle_task_wakeup(task);
- // Running that task, may mark a scope higher up as dirty. If it does, return from the function early
- self.queue_events();
- if self.has_dirty_scopes() {
- return;
- }
- }
- // At this point, we have finished running all tasks that are pending and we haven't found any scopes to rerun. This means it is safe to run our lowest priority work: effects
- while let Some(effect) = self.pop_effect() {
- effect.run(&self.runtime);
- // Check if any new scopes are queued for rerun
- self.queue_events();
- if self.has_dirty_scopes() {
- return;
- }
- }
- }
- }
- /// Rebuild the virtualdom without handling any of the mutations
- ///
- /// This is useful for testing purposes and in cases where you render the output of the virtualdom without
- /// handling any of its mutations.
- pub fn rebuild_in_place(&mut self) {
- self.rebuild(&mut NoOpMutations);
- }
- /// [`VirtualDom::rebuild`] to a vector of mutations for testing purposes
- pub fn rebuild_to_vec(&mut self) -> Mutations {
- let mut mutations = Mutations::default();
- self.rebuild(&mut mutations);
- mutations
- }
- /// 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 run 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, no_run
- /// # use dioxus::prelude::*;
- /// # use dioxus_core::*;
- /// fn app() -> Element {
- /// rsx! { "hello world" }
- /// }
- ///
- /// let mut dom = VirtualDom::new(app);
- /// let mut mutations = Mutations::default();
- /// dom.rebuild(&mut mutations);
- /// ```
- #[instrument(skip(self, to), level = "trace", name = "VirtualDom::rebuild")]
- pub fn rebuild(&mut self, to: &mut impl WriteMutations) {
- let _runtime = RuntimeGuard::new(self.runtime.clone());
- let new_nodes = self.run_scope(ScopeId::ROOT);
- self.scopes[ScopeId::ROOT.0].last_rendered_node = Some(new_nodes.clone());
- // Rebuilding implies we append the created elements to the root
- let m = self.create_scope(Some(to), ScopeId::ROOT, new_nodes, None);
- to.append_children(ElementId(0), m);
- }
- /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress
- /// suspended subtrees.
- #[instrument(skip(self, to), level = "trace", name = "VirtualDom::render_immediate")]
- pub fn render_immediate(&mut self, to: &mut impl WriteMutations) {
- // Process any events that might be pending in the queue
- // Signals marked with .write() need a chance to be handled by the effect driver
- // This also processes futures which might progress into immediately rerunning a scope
- self.process_events();
- // Next, diff any dirty scopes
- // We choose not to poll the deadline since we complete pretty quickly anyways
- let _runtime = RuntimeGuard::new(self.runtime.clone());
- while let Some(work) = self.pop_work() {
- match work {
- Work::PollTask(task) => {
- _ = self.runtime.handle_task_wakeup(task);
- // Make sure we process any new events
- self.queue_events();
- }
- Work::RerunScope(scope) => {
- // If the scope is dirty, run the scope and get the mutations
- self.run_and_diff_scope(Some(to), scope.id);
- }
- }
- }
- self.runtime.finish_render();
- }
- /// [`Self::render_immediate`] to a vector of mutations for testing purposes
- pub fn render_immediate_to_vec(&mut self) -> Mutations {
- let mut mutations = Mutations::default();
- self.render_immediate(&mut mutations);
- mutations
- }
- /// 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
- ///
- /// We don't call "flush_sync" here since there's no sync work to be done. Futures will be progressed like usual,
- /// however any futures waiting on flush_sync will remain pending
- #[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_suspense")]
- pub async fn wait_for_suspense(&mut self) {
- loop {
- if !self.suspended_tasks_remaining() {
- break;
- }
- self.wait_for_suspense_work().await;
- self.render_suspense_immediate().await;
- }
- }
- /// Check if there are any suspended tasks remaining
- pub fn suspended_tasks_remaining(&self) -> bool {
- self.runtime.suspended_tasks.get() > 0
- }
- /// Wait for the scheduler to have any work that should be run during suspense.
- pub async fn wait_for_suspense_work(&mut self) {
- // Wait for a work to be ready (IE new suspense leaves to pop up)
- loop {
- // Process all events - Scopes are marked dirty, etc
- // Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
- self.queue_events();
- // Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
- if self.has_dirty_scopes() {
- break;
- }
- {
- // Make sure we set the runtime since we're running user code
- let _runtime = RuntimeGuard::new(self.runtime.clone());
- // Next, run any queued tasks
- // We choose not to poll the deadline since we complete pretty quickly anyways
- let mut tasks_polled = 0;
- while let Some(task) = self.pop_task() {
- if self.runtime.task_runs_during_suspense(task) {
- let _ = self.runtime.handle_task_wakeup(task);
- // Running that task, may mark a scope higher up as dirty. If it does, return from the function early
- self.queue_events();
- if self.has_dirty_scopes() {
- return;
- }
- }
- tasks_polled += 1;
- // Once we have polled a few tasks, we manually yield to the scheduler to give it a chance to run other pending work
- if tasks_polled > 32 {
- yield_now().await;
- tasks_polled = 0;
- }
- }
- }
- self.wait_for_event().await;
- }
- }
- /// Render any dirty scopes immediately, but don't poll any futures that are client only on that scope
- /// Returns a list of suspense boundaries that were resolved
- pub async fn render_suspense_immediate(&mut self) -> Vec<ScopeId> {
- // Queue any new events before we start working
- self.queue_events();
- // Render whatever work needs to be rendered, unlocking new futures and suspense leaves
- let _runtime = RuntimeGuard::new(self.runtime.clone());
- let mut work_done = 0;
- while let Some(work) = self.pop_work() {
- match work {
- Work::PollTask(task) => {
- // During suspense, we only want to run tasks that are suspended
- if self.runtime.task_runs_during_suspense(task) {
- let _ = self.runtime.handle_task_wakeup(task);
- }
- }
- Work::RerunScope(scope) => {
- let scope_id: ScopeId = scope.id;
- let run_scope = self
- .runtime
- .get_state(scope.id)
- .filter(|scope| scope.should_run_during_suspense())
- .is_some();
- if run_scope {
- // If the scope is dirty, run the scope and get the mutations
- self.run_and_diff_scope(None::<&mut NoOpMutations>, scope_id);
- tracing::trace!("Ran scope {:?} during suspense", scope_id);
- } else {
- tracing::warn!(
- "Scope {:?} was marked as dirty, but will not rerun during suspense. Only nodes that are under a suspense boundary rerun during suspense",
- scope_id
- );
- }
- }
- }
- // Queue any new events
- self.queue_events();
- work_done += 1;
- // Once we have polled a few tasks, we manually yield to the scheduler to give it a chance to run other pending work
- if work_done > 32 {
- yield_now().await;
- work_done = 0;
- }
- }
- self.resolved_scopes
- .sort_by_key(|&id| self.runtime.get_state(id).unwrap().height);
- std::mem::take(&mut self.resolved_scopes)
- }
- /// Get the current runtime
- pub fn runtime(&self) -> Rc<Runtime> {
- 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
- */
- #[instrument(
- skip(self, uievent),
- level = "trace",
- name = "VirtualDom::handle_bubbling_event"
- )]
- fn handle_bubbling_event(&mut self, parent: ElementRef, name: &str, uievent: Event<dyn Any>) {
- // If the event bubbles, we traverse through the tree until we find the target element.
- // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
- let mut parent = Some(parent);
- while let Some(path) = parent {
- let mut listeners = vec![];
- let Some(mount) = self.mounts.get(path.mount.0) else {
- // If the node is suspended and not mounted, we can just ignore the event
- return;
- };
- let el_ref = &mount.node;
- let node_template = el_ref.template;
- let target_path = path.path;
- // Accumulate listeners into the listener list bottom to top
- for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
- let attrs = &*el_ref.dynamic_attrs[idx];
- for attr in attrs.iter() {
- // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
- if attr.name.get(2..) == Some(name) && target_path.is_descendant(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
- tracing::event!(
- tracing::Level::TRACE,
- "Calling {} listeners",
- listeners.len()
- );
- for listener in listeners.into_iter().rev() {
- if let AttributeValue::Listener(listener) = listener {
- self.runtime.rendering.set(false);
- listener.call(uievent.clone());
- self.runtime.rendering.set(true);
- if !uievent.propagates.get() {
- return;
- }
- }
- }
- let mount = el_ref.mount.get().as_usize();
- parent = mount.and_then(|id| self.mounts.get(id).and_then(|el| el.parent));
- }
- }
- /// Call an event listener in the simplest way possible without bubbling upwards
- #[instrument(
- skip(self, uievent),
- level = "trace",
- name = "VirtualDom::handle_non_bubbling_event"
- )]
- fn handle_non_bubbling_event(&mut self, node: ElementRef, name: &str, uievent: Event<dyn Any>) {
- let Some(mount) = self.mounts.get(node.mount.0) else {
- // If the node is suspended and not mounted, we can just ignore the event
- return;
- };
- let el_ref = &mount.node;
- let node_template = el_ref.template;
- let target_path = node.path;
- for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
- let attrs = &*el_ref.dynamic_attrs[idx];
- for attr in attrs.iter() {
- // 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.get(2..) == Some(name) && target_path == this_path {
- if let AttributeValue::Listener(listener) = &attr.value {
- self.runtime.rendering.set(false);
- listener.call(uievent.clone());
- self.runtime.rendering.set(true);
- break;
- }
- }
- }
- }
- }
- }
- impl Drop for VirtualDom {
- fn drop(&mut self) {
- // Drop all scopes in order of height
- let mut scopes = self.scopes.drain().collect::<Vec<_>>();
- scopes.sort_by_key(|scope| scope.state().height);
- for scope in scopes.into_iter().rev() {
- drop(scope);
- }
- }
- }
- /// Yield control back to the async scheduler. This is used to give the scheduler a chance to run other pending work. Or cancel the task if the client has disconnected.
- async fn yield_now() {
- let mut yielded = false;
- std::future::poll_fn::<(), _>(move |cx| {
- if !yielded {
- cx.waker().wake_by_ref();
- yielded = true;
- std::task::Poll::Pending
- } else {
- std::task::Poll::Ready(())
- }
- })
- .await;
- }
|