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}