Przeglądaj źródła

wip: think about dyn any for ScopeState

Jonathan Kelley 2 lat temu
rodzic
commit
12808ec0aa

+ 0 - 11
packages/core/src/events.rs

@@ -158,14 +158,3 @@ impl<T> EventHandler<'_, T> {
         self.callback.replace(None);
     }
 }
-
-#[test]
-fn matches_slice() {
-    let left = &[1, 2, 3];
-    let right = &[1, 2, 3, 4, 5];
-    assert!(is_path_ascendant(left, right));
-    assert!(!is_path_ascendant(right, left));
-    assert!(!is_path_ascendant(left, left));
-
-    assert!(is_path_ascendant(&[1, 2], &[1, 2, 3, 4, 5]));
-}

+ 1 - 1
packages/core/src/scope_arena.rs

@@ -36,7 +36,7 @@ impl VirtualDom {
             spawned_tasks: Default::default(),
             render_cnt: Default::default(),
             hook_arena: Default::default(),
-            hook_vals: Default::default(),
+            hook_list: Default::default(),
             hook_idx: Default::default(),
             shared_contexts: Default::default(),
             tasks: self.scheduler.clone(),

+ 87 - 116
packages/core/src/scopes.rs

@@ -1,14 +1,3 @@
-use std::{
-    any::{Any, TypeId},
-    cell::{Cell, RefCell},
-    collections::{HashMap, HashSet},
-    rc::Rc,
-    sync::Arc,
-};
-
-use bumpalo::Bump;
-use std::future::Future;
-
 use crate::{
     any_props::AnyProps,
     arena::ElementId,
@@ -18,17 +7,40 @@ use crate::{
     nodes::VNode,
     TaskId,
 };
+use bumpalo::Bump;
+use std::future::Future;
+use std::{
+    any::{Any, TypeId},
+    cell::{Cell, RefCell},
+    collections::{HashMap, HashSet},
+    rc::Rc,
+    sync::Arc,
+};
 
+/// A wrapper around the [`Scoped`] object that contains a reference to the [`ScopeState`] and properties for a given
+/// component.
+///
+/// The [`Scope`] is your handle to the [`VirtualDom`] and the component state. Every component is given its own
+/// [`ScopeState`] and merged with its properties to create a [`Scoped`].
+///
+/// The [`Scope`] handle specifically exists to provide a stable reference to these items for the lifetime of the
+/// component render.
 pub type Scope<'a, T = ()> = &'a Scoped<'a, T>;
 
+/// A wrapper around a component's [`ScopeState`] and properties. The [`ScopeState`] provides the majority of methods
+/// for the VirtualDom and component state.
 pub struct Scoped<'a, T = ()> {
+    /// The component's state and handle to the scheduler.
+    ///
+    /// Stores things like the custom bump arena, spawn functions, hooks, and the scheduler.
     pub scope: &'a ScopeState,
+
+    /// The component's properties.
     pub props: &'a T,
 }
 
 impl<'a, T> std::ops::Deref for Scoped<'a, T> {
     type Target = &'a ScopeState;
-
     fn deref(&self) -> &Self::Target {
         &self.scope
     }
@@ -42,6 +54,9 @@ impl<'a, T> std::ops::Deref for Scoped<'a, T> {
 #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
 pub struct ScopeId(pub usize);
 
+/// A component's state.
+///
+/// This struct stores all the important information about a component's state without the props.
 pub struct ScopeState {
     pub(crate) render_cnt: Cell<usize>,
 
@@ -55,7 +70,7 @@ pub struct ScopeState {
     pub(crate) height: u32,
 
     pub(crate) hook_arena: Bump,
-    pub(crate) hook_vals: RefCell<Vec<*mut dyn Any>>,
+    pub(crate) hook_list: RefCell<Vec<*mut dyn Any>>,
     pub(crate) hook_idx: Cell<usize>,
 
     pub(crate) shared_contexts: RefCell<HashMap<TypeId, Box<dyn Any>>>,
@@ -75,6 +90,7 @@ impl ScopeState {
             _ => unreachable!(),
         }
     }
+
     pub fn previous_frame(&self) -> &BumpFrame {
         match self.render_cnt.get() % 2 {
             1 => &self.node_arena_1,
@@ -83,10 +99,19 @@ impl ScopeState {
         }
     }
 
+    /// Get a handle to the currently active bump arena for this Scope
+    ///
+    /// This is a bump memory allocator. Be careful using this directly since the contents will be wiped on the next render.
+    /// It's easy to leak memory here since the drop implementation will not be called for any objects allocated in this arena.
+    ///
+    /// If you need to allocate items that need to be dropped, use bumpalo's box.
     pub fn bump(&self) -> &Bump {
         &self.current_frame().bump
     }
 
+    /// Get a handle to the currently active head node arena for this Scope
+    ///
+    /// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR.
     pub fn root_node<'a>(&'a self) -> &'a VNode<'a> {
         let r = unsafe { &*self.current_frame().node.get() };
         unsafe { std::mem::transmute(r) }
@@ -166,6 +191,7 @@ impl ScopeState {
         Arc::new(move |id| drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
     }
 
+    /// Mark this scope as dirty, and schedule a render for it.
     pub fn needs_update(&self) {
         self.needs_update_any(self.scope_id());
     }
@@ -180,6 +206,42 @@ impl ScopeState {
             .expect("Scheduler to exist if scope exists");
     }
 
+    /// Return any context of type T if it exists on this scope
+    pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
+        self.shared_contexts
+            .borrow()
+            .get(&TypeId::of::<T>())
+            .and_then(|shared| shared.downcast_ref::<T>())
+            .cloned()
+    }
+
+    /// Try to retrieve a shared state with type `T` from any parent scope.
+    ///
+    /// The state will be cloned and returned, if it exists.
+    ///
+    /// We recommend wrapping the state in an `Rc` or `Arc` to avoid deep cloning.
+    pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
+        if let Some(this_ctx) = self.has_context() {
+            return Some(this_ctx);
+        }
+
+        let mut search_parent = self.parent;
+        while let Some(parent_ptr) = search_parent {
+            // safety: all parent pointers are valid thanks to the bump arena
+            let parent = unsafe { &*parent_ptr };
+            if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
+                return Some(
+                    shared
+                        .downcast_ref::<T>()
+                        .expect("Context of type T should exist")
+                        .clone(),
+                );
+            }
+            search_parent = parent.parent;
+        }
+        None
+    }
+
     /// This method enables the ability to expose state to children further down the [`VirtualDom`] Tree.
     ///
     /// This is a "fundamental" operation and should only be called during initialization of a hook.
@@ -212,96 +274,6 @@ impl ScopeState {
         value
     }
 
-    /// Provide a context for the root component from anywhere in your app.
-    ///
-    ///
-    /// # Example
-    ///
-    /// ```rust, ignore
-    /// struct SharedState(&'static str);
-    ///
-    /// static App: Component = |cx| {
-    ///     cx.use_hook(|| cx.provide_root_context(SharedState("world")));
-    ///     render!(Child {})
-    /// }
-    ///
-    /// static Child: Component = |cx| {
-    ///     let state = cx.consume_state::<SharedState>();
-    ///     render!(div { "hello {state.0}" })
-    /// }
-    /// ```
-    pub fn provide_root_context<T: 'static + Clone>(&self, value: T) -> T {
-        // if we *are* the root component, then we can just provide the context directly
-        if self.scope_id() == ScopeId(0) {
-            self.shared_contexts
-                .borrow_mut()
-                .insert(TypeId::of::<T>(), Box::new(value.clone()))
-                .and_then(|f| f.downcast::<T>().ok());
-            return value;
-        }
-
-        let mut search_parent = self.parent;
-
-        while let Some(parent) = search_parent.take() {
-            let parent = unsafe { &*parent };
-
-            if parent.scope_id() == ScopeId(0) {
-                let _ = parent
-                    .shared_contexts
-                    .borrow_mut()
-                    .insert(TypeId::of::<T>(), Box::new(value.clone()));
-
-                return value;
-            }
-
-            search_parent = parent.parent;
-        }
-
-        unreachable!("all apps have a root scope")
-    }
-
-    /// Try to retrieve a shared state with type T from the any parent Scope.
-    pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
-        if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::<T>()) {
-            Some(
-                (*shared
-                    .downcast_ref::<T>()
-                    .expect("Context of type T should exist"))
-                .clone(),
-            )
-        } else {
-            let mut search_parent = self.parent;
-
-            while let Some(parent_ptr) = search_parent {
-                // safety: all parent pointers are valid thanks to the bump arena
-                let parent = unsafe { &*parent_ptr };
-                if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
-                    return Some(
-                        shared
-                            .downcast_ref::<T>()
-                            .expect("Context of type T should exist")
-                            .clone(),
-                    );
-                }
-                search_parent = parent.parent;
-            }
-            None
-        }
-    }
-
-    /// Return any context of type T if it exists on this scope
-    pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
-        match self.shared_contexts.borrow().get(&TypeId::of::<T>()) {
-            Some(shared) => Some(
-                (*shared
-                    .downcast_ref::<T>()
-                    .expect("Context of type T should exist"))
-                .clone(),
-            ),
-            None => None,
-        }
-    }
-
     /// Pushes the future onto the poll queue to be polled after the component renders.
     pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
         self.tasks.spawn(self.id, fut)
@@ -312,7 +284,7 @@ impl ScopeState {
         self.push_future(fut);
     }
 
-    /// Spawn a future that Dioxus will never clean up
+    /// Spawn a future that Dioxus won't clean up when this component is unmounted
     ///
     /// This is good for tasks that need to be run after the component has been dropped.
     pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
@@ -328,13 +300,14 @@ impl ScopeState {
         id
     }
 
-    /// Informs the scheduler that this task is no longer needed and should be removed
-    /// on next poll.
+    /// Informs the scheduler that this task is no longer needed and should be removed.
+    ///
+    /// This drops the task immediately.
     pub fn remove_future(&self, id: TaskId) {
         self.tasks.remove(id);
     }
 
-    /// Take a lazy [`VNode`] structure and actually build it with the context of the Vdoms efficient [`VNode`] allocator.
+    /// Take a lazy [`VNode`] structure and actually build it with the context of the efficient [`Bump`] allocator.
     ///
     /// ## Example
     ///
@@ -369,19 +342,17 @@ impl ScopeState {
     /// ```
     #[allow(clippy::mut_from_ref)]
     pub fn use_hook<State: 'static>(&self, initializer: impl FnOnce() -> State) -> &mut State {
-        let mut vals = self.hook_vals.borrow_mut();
-
-        let hook_len = vals.len();
-        let cur_idx = self.hook_idx.get();
+        let cur_hook = self.hook_idx.get();
+        let mut hook_list = self.hook_list.borrow_mut();
 
-        if cur_idx >= hook_len {
-            vals.push(self.hook_arena.alloc(initializer()));
+        if cur_hook >= hook_list.len() {
+            hook_list.push(self.hook_arena.alloc(initializer()));
         }
 
-        vals
-            .get(cur_idx)
+        hook_list
+            .get(cur_hook)
             .and_then(|inn| {
-                self.hook_idx.set(cur_idx + 1);
+                self.hook_idx.set(cur_hook + 1);
                 let raw_box = unsafe { &mut **inn };
                 raw_box.downcast_mut::<State>()
             })

+ 29 - 10
packages/core/src/virtual_dom.rs

@@ -1,3 +1,7 @@
+//! # Virtual DOM Implementation for Rust
+//!
+//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
+
 use crate::{
     any_props::VComponentProps,
     arena::ElementId,
@@ -13,12 +17,13 @@ use crate::{
 };
 use futures_util::{pin_mut, StreamExt};
 use slab::Slab;
-use std::rc::Rc;
 use std::{
     any::Any,
+    cell::Cell,
     collections::{BTreeSet, HashMap},
+    future::Future,
+    rc::Rc,
 };
-use std::{cell::Cell, future::Future};
 
 /// A virtual node system that progresses user events and diffs UI trees.
 ///
@@ -245,9 +250,11 @@ impl VirtualDom {
 
         // The root component is always a suspense boundary for any async children
         // This could be unexpected, so we might rethink this behavior later
+        //
+        // We *could* just panic if the suspense boundary is not found
         root.provide_context(SuspenseBoundary::new(ScopeId(0)));
 
-        // the root element is always given element 0
+        // the root element is always given element ID 0 since it's the container for the entire tree
         dom.elements.insert(ElementRef::null());
 
         dom
@@ -269,19 +276,24 @@ impl VirtualDom {
 
     /// 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
+    /// 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 VirtualDom "works", it will re-render this scope
     pub fn mark_dirty_scope(&mut self, id: ScopeId) {
         let height = self.scopes[id.0].height;
         self.dirty_scopes.insert(DirtyScope { height, id });
     }
 
     /// Determine whether or not a scope is currently in a suspended state
+    ///
+    /// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is
+    /// currently suspended.
     pub fn is_scope_suspended(&self, id: ScopeId) -> bool {
         !self.scopes[id.0]
             .consume_context::<SuspenseContext>()
@@ -291,7 +303,7 @@ impl VirtualDom {
             .is_empty()
     }
 
-    /// Determine is the tree is at all suspended. Used by SSR and other outside mechanisms to determine if the tree is
+    /// Determine if the tree is at all suspended. Used by SSR and other outside mechanisms to determine if the tree is
     /// ready to be rendered.
     pub fn has_suspended_work(&self) -> bool {
         !self.scheduler.leaves.borrow().is_empty()
@@ -324,6 +336,10 @@ impl VirtualDom {
 
         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)
@@ -354,6 +370,7 @@ impl VirtualDom {
                 let this_path = template.template.attr_paths[idx];
 
                 // listeners are required to be prefixed with "on", but they come back to the virtualdom with that missing
+                // we should fix this so that we look for "onclick" instead of "click"
                 if &attr.name[2..] == name && is_path_ascendant(&target_path, &this_path) {
                     listeners.push(&attr.value);
 
@@ -362,7 +379,9 @@ impl VirtualDom {
                         break;
                     }
 
-                    // Break if this is the exact target element
+                    // 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 this_path == target_path {
                         break;
                     }
@@ -392,7 +411,7 @@ impl VirtualDom {
     ///
     /// This method is cancel-safe, so you're fine to discard the future in a select block.
     ///
-    /// This lets us poll async tasks during idle periods without blocking the main thread.
+    /// This lets us poll async tasks and suspended trees during idle periods without blocking the main thread.
     ///
     /// # Example
     ///
@@ -433,7 +452,7 @@ impl VirtualDom {
 
     /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch.
     ///
-    /// The diff machine expects the RealDom's stack to be the root of the application.
+    /// 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.
@@ -498,7 +517,6 @@ impl VirtualDom {
         &'a mut self,
         deadline: impl Future<Output = ()>,
     ) -> Mutations<'a> {
-        use futures_util::future::{select, Either};
         pin_mut!(deadline);
 
         let mut mutations = Mutations::new(0);
@@ -533,7 +551,7 @@ impl VirtualDom {
 
             // Wait for suspense, or a deadline
             if self.dirty_scopes.is_empty() {
-                // If there's no suspense, then we have no reason to wait
+                // If there's no pending suspense, then we have no reason to wait
                 if self.scheduler.leaves.borrow().is_empty() {
                     return mutations;
                 }
@@ -543,6 +561,7 @@ impl VirtualDom {
                 pin_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, work).await {
                     return mutations;
                 }

+ 2 - 1
packages/core/tests/task.rs

@@ -1,6 +1,7 @@
-use std::time::Duration;
+//! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely
 
 use dioxus_core::*;
+use std::time::Duration;
 
 #[tokio::test]
 async fn it_works() {