dioxus_core/
virtual_dom.rs

1//! # Virtual DOM Implementation for Rust
2//!
3//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
4
5use crate::properties::RootProps;
6use crate::root_wrapper::RootScopeWrapper;
7use crate::{
8    arena::ElementId,
9    innerlude::{NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState, VProps, WriteMutations},
10    runtime::{Runtime, RuntimeGuard},
11    scopes::ScopeId,
12    ComponentFunction, Element, Mutations,
13};
14use crate::{innerlude::Work, scopes::LastRenderedNode};
15use crate::{Task, VComponent};
16use futures_util::StreamExt;
17use slab::Slab;
18use std::collections::BTreeSet;
19use std::{any::Any, rc::Rc};
20use tracing::instrument;
21
22/// A virtual node system that progresses user events and diffs UI trees.
23///
24/// ## Guide
25///
26/// Components are defined as simple functions that take [`crate::properties::Properties`] and return an [`Element`].
27///
28/// ```rust
29/// # use dioxus::prelude::*;
30///
31/// #[derive(Props, PartialEq, Clone)]
32/// struct AppProps {
33///     title: String
34/// }
35///
36/// fn app(cx: AppProps) -> Element {
37///     rsx!(
38///         div {"hello, {cx.title}"}
39///     )
40/// }
41/// ```
42///
43/// Components may be composed to make complex apps.
44///
45/// ```rust
46/// # #![allow(unused)]
47/// # use dioxus::prelude::*;
48///
49/// # #[derive(Props, PartialEq, Clone)]
50/// # struct AppProps {
51/// #     title: String
52/// # }
53///
54/// static ROUTES: &str = "";
55///
56/// #[component]
57/// fn app(cx: AppProps) -> Element {
58///     rsx!(
59///         NavBar { routes: ROUTES }
60///         Title { "{cx.title}" }
61///         Footer {}
62///     )
63/// }
64///
65/// #[component]
66/// fn NavBar( routes: &'static str) -> Element {
67///     rsx! {
68///         div { "Routes: {routes}" }
69///     }
70/// }
71///
72/// #[component]
73/// fn Footer() -> Element {
74///     rsx! { div { "Footer" } }
75/// }
76///
77/// #[component]
78/// fn Title( children: Element) -> Element {
79///     rsx! {
80///         div { id: "title", {children} }
81///     }
82/// }
83/// ```
84///
85/// To start an app, create a [`VirtualDom`] and call [`VirtualDom::rebuild`] to get the list of edits required to
86/// draw the UI.
87///
88/// ```rust
89/// # use dioxus::prelude::*;
90/// # use dioxus_core::*;
91/// # fn app() -> Element { rsx! { div {} } }
92///
93/// let mut vdom = VirtualDom::new(app);
94/// let edits = vdom.rebuild_to_vec();
95/// ```
96///
97/// To call listeners inside the VirtualDom, call [`Runtime::handle_event`] with the appropriate event data.
98///
99/// ```rust, no_run
100/// # use dioxus::prelude::*;
101/// # use dioxus_core::*;
102/// # fn app() -> Element { rsx! { div {} } }
103/// # let mut vdom = VirtualDom::new(app);
104/// # let runtime = vdom.runtime();
105/// let event = Event::new(std::rc::Rc::new(0) as std::rc::Rc<dyn std::any::Any>, true);
106/// runtime.handle_event("onclick", event, ElementId(0));
107/// ```
108///
109/// While no events are ready, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom.
110///
111/// ```rust, no_run
112/// # use dioxus::prelude::*;
113/// # use dioxus_core::*;
114/// # fn app() -> Element { rsx! { div {} } }
115/// # let mut vdom = VirtualDom::new(app);
116/// tokio::runtime::Runtime::new().unwrap().block_on(async {
117///     vdom.wait_for_work().await;
118/// });
119/// ```
120///
121/// Once work is ready, call [`VirtualDom::render_immediate`] to compute the differences between the previous and
122/// current UI trees. This will write edits to a [`WriteMutations`] object you pass in that contains with edits that need to be
123/// handled by the renderer.
124///
125/// ```rust, no_run
126/// # use dioxus::prelude::*;
127/// # use dioxus_core::*;
128/// # fn app() -> Element { rsx! { div {} } }
129/// # let mut vdom = VirtualDom::new(app);
130/// let mut mutations = Mutations::default();
131///
132/// vdom.render_immediate(&mut mutations);
133/// ```
134///
135/// To not wait for suspense while diffing the VirtualDom, call [`VirtualDom::render_immediate`].
136///
137///
138/// ## Building an event loop around Dioxus:
139///
140/// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above.
141/// ```rust, no_run
142/// # use dioxus::prelude::*;
143/// # use dioxus_core::*;
144/// # struct RealDom;
145/// # struct Event {}
146/// # impl RealDom {
147/// #     fn new() -> Self {
148/// #         Self {}
149/// #     }
150/// #     fn apply(&mut self) -> Mutations {
151/// #         unimplemented!()
152/// #     }
153/// #     async fn wait_for_event(&mut self) -> std::rc::Rc<dyn std::any::Any> {
154/// #         unimplemented!()
155/// #     }
156/// # }
157/// #
158/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
159/// let mut real_dom = RealDom::new();
160///
161/// #[component]
162/// fn app() -> Element {
163///     rsx! {
164///         div { "Hello World" }
165///     }
166/// }
167///
168/// let mut dom = VirtualDom::new(app);
169///
170/// dom.rebuild(&mut real_dom.apply());
171///
172/// loop {
173///     tokio::select! {
174///         _ = dom.wait_for_work() => {}
175///         evt = real_dom.wait_for_event() => {
176///             let evt = dioxus_core::Event::new(evt, true);
177///             dom.runtime().handle_event("onclick", evt, ElementId(0))
178///         },
179///     }
180///
181///     dom.render_immediate(&mut real_dom.apply());
182/// }
183/// # });
184/// ```
185///
186/// ## Waiting for suspense
187///
188/// Because Dioxus supports suspense, you can use it for server-side rendering, static site generation, and other use cases
189/// where waiting on portions of the UI to finish rendering is important. To wait for suspense, use the
190/// [`VirtualDom::wait_for_suspense`] method:
191///
192/// ```rust, no_run
193/// # use dioxus::prelude::*;
194/// # use dioxus_core::*;
195/// # fn app() -> Element { rsx! { div {} } }
196/// tokio::runtime::Runtime::new().unwrap().block_on(async {
197///     let mut dom = VirtualDom::new(app);
198///
199///     dom.rebuild_in_place();
200///     dom.wait_for_suspense().await;
201/// });
202///
203/// // Render the virtual dom
204/// ```
205pub struct VirtualDom {
206    pub(crate) scopes: Slab<ScopeState>,
207
208    pub(crate) dirty_scopes: BTreeSet<ScopeOrder>,
209
210    pub(crate) runtime: Rc<Runtime>,
211
212    // The scopes that have been resolved since the last render
213    pub(crate) resolved_scopes: Vec<ScopeId>,
214
215    rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
216}
217
218impl VirtualDom {
219    /// Create a new VirtualDom with a component that does not have special props.
220    ///
221    /// # Description
222    ///
223    /// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
224    ///
225    /// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
226    /// to toss out the entire tree.
227    ///
228    ///
229    /// # Example
230    /// ```rust, no_run
231    /// # use dioxus::prelude::*;
232    /// # use dioxus_core::*;
233    /// fn Example() -> Element  {
234    ///     rsx!( div { "hello world" } )
235    /// }
236    ///
237    /// let dom = VirtualDom::new(Example);
238    /// ```
239    ///
240    /// Note: the VirtualDom is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it.
241    pub fn new(app: fn() -> Element) -> Self {
242        Self::new_with_props(app, ())
243    }
244
245    /// Create a new VirtualDom with the given properties for the root component.
246    ///
247    /// # Description
248    ///
249    /// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
250    ///
251    /// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
252    /// to toss out the entire tree.
253    ///
254    ///
255    /// # Example
256    /// ```rust, no_run
257    /// # use dioxus::prelude::*;
258    /// # use dioxus_core::*;
259    /// #[derive(PartialEq, Props, Clone)]
260    /// struct SomeProps {
261    ///     name: &'static str
262    /// }
263    ///
264    /// fn Example(cx: SomeProps) -> Element  {
265    ///     rsx! { div { "hello {cx.name}" } }
266    /// }
267    ///
268    /// let dom = VirtualDom::new_with_props(Example, SomeProps { name: "world" });
269    /// ```
270    ///
271    /// Note: the VirtualDom is not progressed on creation. You must either "run_with_deadline" or use "rebuild" to progress it.
272    ///
273    /// ```rust, no_run
274    /// # use dioxus::prelude::*;
275    /// # use dioxus_core::*;
276    /// # #[derive(PartialEq, Props, Clone)]
277    /// # struct SomeProps {
278    /// #     name: &'static str
279    /// # }
280    /// # fn Example(cx: SomeProps) -> Element  {
281    /// #     rsx! { div { "hello {cx.name}" } }
282    /// # }
283    /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
284    /// dom.rebuild_in_place();
285    /// ```
286    pub fn new_with_props<P: Clone + 'static, M: 'static>(
287        root: impl ComponentFunction<P, M>,
288        root_props: P,
289    ) -> Self {
290        let render_fn = root.fn_ptr();
291        let props = VProps::new(root, |_, _| true, root_props, "Root");
292        Self::new_with_component(VComponent {
293            name: "root",
294            render_fn,
295            props: Box::new(props),
296        })
297    }
298
299    /// Create a new virtualdom and build it immediately
300    pub fn prebuilt(app: fn() -> Element) -> Self {
301        let mut dom = Self::new(app);
302        dom.rebuild_in_place();
303        dom
304    }
305
306    /// Create a new VirtualDom from a VComponent
307    #[instrument(skip(root), level = "trace", name = "VirtualDom::new")]
308    pub(crate) fn new_with_component(root: VComponent) -> Self {
309        let (tx, rx) = futures_channel::mpsc::unbounded();
310
311        let mut dom = Self {
312            rx,
313            runtime: Runtime::new(tx),
314            scopes: Default::default(),
315            dirty_scopes: Default::default(),
316            resolved_scopes: Default::default(),
317        };
318
319        let root = VProps::new(
320            RootScopeWrapper,
321            |_, _| true,
322            RootProps(root),
323            "RootWrapper",
324        );
325        dom.new_scope(Box::new(root), "app");
326
327        #[cfg(debug_assertions)]
328        dom.register_subsecond_handler();
329
330        dom
331    }
332
333    /// Get the state for any scope given its ID
334    ///
335    /// This is useful for inserting or removing contexts from a scope, or rendering out its root node
336    pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
337        self.scopes.get(id.0)
338    }
339
340    /// Get the single scope at the top of the VirtualDom tree that will always be around
341    ///
342    /// This scope has a ScopeId of 0 and is the root of the tree
343    pub fn base_scope(&self) -> &ScopeState {
344        self.get_scope(ScopeId::ROOT).unwrap()
345    }
346
347    /// Run a closure inside the dioxus runtime
348    #[instrument(skip(self, f), level = "trace", name = "VirtualDom::in_runtime")]
349    pub fn in_runtime<O>(&self, f: impl FnOnce() -> O) -> O {
350        let _runtime = RuntimeGuard::new(self.runtime.clone());
351        f()
352    }
353
354    /// Run a closure inside a specific scope
355    pub fn in_scope<T>(&self, scope: ScopeId, f: impl FnOnce() -> T) -> T {
356        self.runtime.in_scope(scope, f)
357    }
358
359    /// Build the virtualdom with a global context inserted into the base scope
360    ///
361    /// This is useful for what is essentially dependency injection when building the app
362    pub fn with_root_context<T: Clone + 'static>(self, context: T) -> Self {
363        self.base_scope().state().provide_context(context);
364        self
365    }
366
367    /// Provide a context to the root scope
368    pub fn provide_root_context<T: Clone + 'static>(&self, context: T) {
369        self.base_scope().state().provide_context(context);
370    }
371
372    /// Build the virtualdom with a global context inserted into the base scope
373    ///
374    /// This method is useful for when you want to provide a context in your app without knowing its type
375    pub fn insert_any_root_context(&mut self, context: Box<dyn Any>) {
376        self.base_scope().state().provide_any_context(context);
377    }
378
379    /// Mark all scopes as dirty. Each scope will be re-rendered.
380    pub fn mark_all_dirty(&mut self) {
381        let mut orders = vec![];
382
383        for (_idx, scope) in self.scopes.iter() {
384            orders.push(ScopeOrder::new(scope.state().height(), scope.id()));
385        }
386
387        for order in orders {
388            self.queue_scope(order);
389        }
390    }
391
392    /// Manually mark a scope as requiring a re-render
393    ///
394    /// Whenever the Runtime "works", it will re-render this scope
395    pub fn mark_dirty(&mut self, id: ScopeId) {
396        let Some(scope) = self.runtime.try_get_state(id) else {
397            return;
398        };
399
400        tracing::event!(tracing::Level::TRACE, "Marking scope {:?} as dirty", id);
401        let order = ScopeOrder::new(scope.height(), id);
402        drop(scope);
403        self.queue_scope(order);
404    }
405
406    /// Mark a task as dirty
407    fn mark_task_dirty(&mut self, task: Task) {
408        let Some(scope) = self.runtime.task_scope(task) else {
409            return;
410        };
411        let Some(scope) = self.runtime.try_get_state(scope) else {
412            return;
413        };
414
415        tracing::event!(
416            tracing::Level::TRACE,
417            "Marking task {:?} (spawned in {:?}) as dirty",
418            task,
419            scope.id,
420        );
421
422        let order = ScopeOrder::new(scope.height(), scope.id);
423        drop(scope);
424        self.queue_task(task, order);
425    }
426
427    /// Wait for the scheduler to have any work.
428    ///
429    /// This method polls the internal future queue, waiting for suspense nodes, tasks, or other work. This completes when
430    /// any work is ready. If multiple scopes are marked dirty from a task or a suspense tree is finished, this method
431    /// will exit.
432    ///
433    /// This method is cancel-safe, so you're fine to discard the future in a select block.
434    ///
435    /// This lets us poll async tasks and suspended trees during idle periods without blocking the main thread.
436    ///
437    /// # Example
438    ///
439    /// ```rust, no_run
440    /// # use dioxus::prelude::*;
441    /// # fn app() -> Element { rsx! { div {} } }
442    /// let dom = VirtualDom::new(app);
443    /// ```
444    #[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_work")]
445    pub async fn wait_for_work(&mut self) {
446        loop {
447            // Process all events - Scopes are marked dirty, etc
448            // Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
449            self.process_events();
450
451            // 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
452            if self.has_dirty_scopes() {
453                return;
454            }
455
456            // Make sure we set the runtime since we're running user code
457            let _runtime = RuntimeGuard::new(self.runtime.clone());
458
459            // There isn't any more work we can do synchronously. Wait for any new work to be ready
460            self.wait_for_event().await;
461        }
462    }
463
464    /// Wait for the next event to trigger and add it to the queue
465    #[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_event")]
466    async fn wait_for_event(&mut self) {
467        match self.rx.next().await.expect("channel should never close") {
468            SchedulerMsg::Immediate(id) => self.mark_dirty(id),
469            SchedulerMsg::TaskNotified(id) => {
470                // Instead of running the task immediately, we insert it into the runtime's task queue.
471                // The task may be marked dirty at the same time as the scope that owns the task is dropped.
472                self.mark_task_dirty(Task::from_id(id));
473            }
474            SchedulerMsg::EffectQueued => {}
475            SchedulerMsg::AllDirty => self.mark_all_dirty(),
476        };
477    }
478
479    /// Queue any pending events
480    fn queue_events(&mut self) {
481        // Prevent a task from deadlocking the runtime by repeatedly queueing itself
482        while let Ok(Some(msg)) = self.rx.try_next() {
483            match msg {
484                SchedulerMsg::Immediate(id) => self.mark_dirty(id),
485                SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)),
486                SchedulerMsg::EffectQueued => {}
487                SchedulerMsg::AllDirty => self.mark_all_dirty(),
488            }
489        }
490    }
491
492    /// Process all events in the queue until there are no more left
493    #[instrument(skip(self), level = "trace", name = "VirtualDom::process_events")]
494    pub fn process_events(&mut self) {
495        self.queue_events();
496
497        // 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
498        if self.has_dirty_scopes() {
499            return;
500        }
501
502        self.poll_tasks()
503    }
504
505    /// Poll any queued tasks
506    #[instrument(skip(self), level = "trace", name = "VirtualDom::poll_tasks")]
507    fn poll_tasks(&mut self) {
508        // Make sure we set the runtime since we're running user code
509        let _runtime = RuntimeGuard::new(self.runtime.clone());
510
511        // Keep polling tasks until there are no more effects or tasks to run
512        // Or until we have no more dirty scopes
513        while !self.runtime.dirty_tasks.borrow().is_empty()
514            || !self.runtime.pending_effects.borrow().is_empty()
515        {
516            // Next, run any queued tasks
517            // We choose not to poll the deadline since we complete pretty quickly anyways
518            while let Some(task) = self.pop_task() {
519                let _ = self.runtime.handle_task_wakeup(task);
520
521                // Running that task, may mark a scope higher up as dirty. If it does, return from the function early
522                self.queue_events();
523                if self.has_dirty_scopes() {
524                    return;
525                }
526            }
527
528            // 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
529            while let Some(effect) = self.pop_effect() {
530                effect.run();
531                // Check if any new scopes are queued for rerun
532                self.queue_events();
533                if self.has_dirty_scopes() {
534                    return;
535                }
536            }
537        }
538    }
539
540    /// Rebuild the virtualdom without handling any of the mutations
541    ///
542    /// This is useful for testing purposes and in cases where you render the output of the virtualdom without
543    /// handling any of its mutations.
544    pub fn rebuild_in_place(&mut self) {
545        self.rebuild(&mut NoOpMutations);
546    }
547
548    /// [`VirtualDom::rebuild`] to a vector of mutations for testing purposes
549    pub fn rebuild_to_vec(&mut self) -> Mutations {
550        let mut mutations = Mutations::default();
551        self.rebuild(&mut mutations);
552        mutations
553    }
554
555    /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch.
556    ///
557    /// The mutations item expects the RealDom's stack to be the root of the application.
558    ///
559    /// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the
560    /// root component will be run once and then diffed. All updates will flow out as mutations.
561    ///
562    /// All state stored in components will be completely wiped away.
563    ///
564    /// Any templates previously registered will remain.
565    ///
566    /// # Example
567    /// ```rust, no_run
568    /// # use dioxus::prelude::*;
569    /// # use dioxus_core::*;
570    /// fn app() -> Element {
571    ///     rsx! { "hello world" }
572    /// }
573    ///
574    /// let mut dom = VirtualDom::new(app);
575    /// let mut mutations = Mutations::default();
576    /// dom.rebuild(&mut mutations);
577    /// ```
578    #[instrument(skip(self, to), level = "trace", name = "VirtualDom::rebuild")]
579    pub fn rebuild(&mut self, to: &mut impl WriteMutations) {
580        let _runtime = RuntimeGuard::new(self.runtime.clone());
581        let new_nodes = self
582            .runtime
583            .clone()
584            .while_rendering(|| self.run_scope(ScopeId::ROOT));
585
586        let new_nodes = LastRenderedNode::new(new_nodes);
587
588        self.scopes[ScopeId::ROOT.0].last_rendered_node = Some(new_nodes.clone());
589
590        // Rebuilding implies we append the created elements to the root
591        let m = self.create_scope(Some(to), ScopeId::ROOT, new_nodes, None);
592
593        to.append_children(ElementId(0), m);
594    }
595
596    /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress
597    /// suspended subtrees.
598    #[instrument(skip(self, to), level = "trace", name = "VirtualDom::render_immediate")]
599    pub fn render_immediate(&mut self, to: &mut impl WriteMutations) {
600        // Process any events that might be pending in the queue
601        // Signals marked with .write() need a chance to be handled by the effect driver
602        // This also processes futures which might progress into immediately rerunning a scope
603        self.process_events();
604
605        // Next, diff any dirty scopes
606        // We choose not to poll the deadline since we complete pretty quickly anyways
607        let _runtime = RuntimeGuard::new(self.runtime.clone());
608        while let Some(work) = self.pop_work() {
609            match work {
610                Work::PollTask(task) => {
611                    _ = self.runtime.handle_task_wakeup(task);
612                    // Make sure we process any new events
613                    self.queue_events();
614                }
615                Work::RerunScope(scope) => {
616                    // If the scope is dirty, run the scope and get the mutations
617                    self.runtime.clone().while_rendering(|| {
618                        self.run_and_diff_scope(Some(to), scope.id);
619                    });
620                }
621            }
622        }
623
624        self.runtime.finish_render();
625    }
626
627    /// [`Self::render_immediate`] to a vector of mutations for testing purposes
628    pub fn render_immediate_to_vec(&mut self) -> Mutations {
629        let mut mutations = Mutations::default();
630        self.render_immediate(&mut mutations);
631        mutations
632    }
633
634    /// Render the virtual dom, waiting for all suspense to be finished
635    ///
636    /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
637    ///
638    /// We don't call "flush_sync" here since there's no sync work to be done. Futures will be progressed like usual,
639    /// however any futures waiting on flush_sync will remain pending
640    #[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_suspense")]
641    pub async fn wait_for_suspense(&mut self) {
642        loop {
643            self.queue_events();
644
645            if !self.suspended_tasks_remaining() && !self.has_dirty_scopes() {
646                break;
647            }
648
649            self.wait_for_suspense_work().await;
650
651            self.render_suspense_immediate().await;
652        }
653    }
654
655    /// Check if there are any suspended tasks remaining
656    pub fn suspended_tasks_remaining(&self) -> bool {
657        self.runtime.suspended_tasks.get() > 0
658    }
659
660    /// Wait for the scheduler to have any work that should be run during suspense.
661    pub async fn wait_for_suspense_work(&mut self) {
662        // Wait for a work to be ready (IE new suspense leaves to pop up)
663        loop {
664            // Process all events - Scopes are marked dirty, etc
665            // Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
666            self.queue_events();
667
668            // 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
669            if self.has_dirty_scopes() {
670                break;
671            }
672
673            {
674                // Make sure we set the runtime since we're running user code
675                let _runtime = RuntimeGuard::new(self.runtime.clone());
676                // Next, run any queued tasks
677                // We choose not to poll the deadline since we complete pretty quickly anyways
678                let mut tasks_polled = 0;
679                while let Some(task) = self.pop_task() {
680                    if self.runtime.task_runs_during_suspense(task) {
681                        let _ = self.runtime.handle_task_wakeup(task);
682                        // Running that task, may mark a scope higher up as dirty. If it does, return from the function early
683                        self.queue_events();
684                        if self.has_dirty_scopes() {
685                            return;
686                        }
687                    }
688                    tasks_polled += 1;
689                    // Once we have polled a few tasks, we manually yield to the scheduler to give it a chance to run other pending work
690                    if tasks_polled > 32 {
691                        yield_now().await;
692                        tasks_polled = 0;
693                    }
694                }
695            }
696
697            self.wait_for_event().await;
698        }
699    }
700
701    /// Render any dirty scopes immediately, but don't poll any futures that are client only on that scope
702    /// Returns a list of suspense boundaries that were resolved
703    pub async fn render_suspense_immediate(&mut self) -> Vec<ScopeId> {
704        // Queue any new events before we start working
705        self.queue_events();
706
707        // Render whatever work needs to be rendered, unlocking new futures and suspense leaves
708        let _runtime = RuntimeGuard::new(self.runtime.clone());
709
710        let mut work_done = 0;
711        while let Some(work) = self.pop_work() {
712            match work {
713                Work::PollTask(task) => {
714                    // During suspense, we only want to run tasks that are suspended
715                    if self.runtime.task_runs_during_suspense(task) {
716                        let _ = self.runtime.handle_task_wakeup(task);
717                    }
718                }
719                Work::RerunScope(scope) => {
720                    let scope_id: ScopeId = scope.id;
721                    let run_scope = self
722                        .runtime
723                        .try_get_state(scope.id)
724                        .filter(|scope| scope.should_run_during_suspense())
725                        .is_some();
726                    if run_scope {
727                        // If the scope is dirty, run the scope and get the mutations
728                        self.runtime.clone().while_rendering(|| {
729                            self.run_and_diff_scope(None::<&mut NoOpMutations>, scope_id);
730                        });
731
732                        tracing::trace!("Ran scope {:?} during suspense", scope_id);
733                    } else {
734                        tracing::warn!(
735                            "Scope {:?} was marked as dirty, but will not rerun during suspense. Only nodes that are under a suspense boundary rerun during suspense",
736                            scope_id
737                        );
738                    }
739                }
740            }
741
742            // Queue any new events
743            self.queue_events();
744            work_done += 1;
745
746            // Once we have polled a few tasks, we manually yield to the scheduler to give it a chance to run other pending work
747            if work_done > 32 {
748                yield_now().await;
749                work_done = 0;
750            }
751        }
752
753        self.resolved_scopes
754            .sort_by_key(|&id| self.runtime.get_state(id).height);
755        std::mem::take(&mut self.resolved_scopes)
756    }
757
758    /// Get the current runtime
759    pub fn runtime(&self) -> Rc<Runtime> {
760        self.runtime.clone()
761    }
762
763    /// Handle an event with the Virtual Dom. This method is deprecated in favor of [VirtualDom::runtime().handle_event] and will be removed in a future release.
764    #[deprecated = "Use [VirtualDom::runtime().handle_event] instead"]
765    pub fn handle_event(&self, name: &str, event: Rc<dyn Any>, element: ElementId, bubbling: bool) {
766        let event = crate::Event::new(event, bubbling);
767        self.runtime().handle_event(name, event, element);
768    }
769
770    #[cfg(debug_assertions)]
771    fn register_subsecond_handler(&self) {
772        let sender = self.runtime().sender.clone();
773        subsecond::register_handler(std::sync::Arc::new(move || {
774            _ = sender.unbounded_send(SchedulerMsg::AllDirty);
775        }));
776    }
777}
778
779impl Drop for VirtualDom {
780    fn drop(&mut self) {
781        // Drop all scopes in order of height
782        let mut scopes = self.scopes.drain().collect::<Vec<_>>();
783        scopes.sort_by_key(|scope| scope.state().height);
784        for scope in scopes.into_iter().rev() {
785            drop(scope);
786        }
787
788        // Drop the mounts, tasks, and effects, releasing any `Rc<Runtime>` references
789        self.runtime.pending_effects.borrow_mut().clear();
790        self.runtime.tasks.borrow_mut().clear();
791        self.runtime.mounts.borrow_mut().clear();
792    }
793}
794
795/// 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.
796async fn yield_now() {
797    let mut yielded = false;
798    std::future::poll_fn::<(), _>(move |cx| {
799        if !yielded {
800            cx.waker().wake_by_ref();
801            yielded = true;
802            std::task::Poll::Pending
803        } else {
804            std::task::Poll::Ready(())
805        }
806    })
807    .await;
808}