From 12808ec0aade4c7f39f93aeeecf884a75a71dd5c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 19 Nov 2022 15:24:28 -0800 Subject: [PATCH] wip: think about dyn any for ScopeState --- packages/core/src/events.rs | 11 -- packages/core/src/scope_arena.rs | 2 +- packages/core/src/scopes.rs | 203 +++++++++++++------------------ packages/core/src/virtual_dom.rs | 39 ++++-- packages/core/tests/task.rs | 3 +- 5 files changed, 119 insertions(+), 139 deletions(-) diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index deb96ffdf..cb14734d2 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -158,14 +158,3 @@ impl 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])); -} diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index cc5783e59..0aa535e10 100644 --- a/packages/core/src/scope_arena.rs +++ b/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(), diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index fef76f558..3291db9e0 100644 --- a/packages/core/src/scopes.rs +++ b/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, @@ -55,7 +70,7 @@ pub struct ScopeState { pub(crate) height: u32, pub(crate) hook_arena: Bump, - pub(crate) hook_vals: RefCell>, + pub(crate) hook_list: RefCell>, pub(crate) hook_idx: Cell, pub(crate) shared_contexts: RefCell>>, @@ -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(&self) -> Option { + self.shared_contexts + .borrow() + .get(&TypeId::of::()) + .and_then(|shared| shared.downcast_ref::()) + .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(&self) -> Option { + 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::()) { + return Some( + shared + .downcast_ref::() + .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::(); - /// render!(div { "hello {state.0}" }) - /// } - /// ``` - pub fn provide_root_context(&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::(), Box::new(value.clone())) - .and_then(|f| f.downcast::().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::(), 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(&self) -> Option { - if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::()) { - Some( - (*shared - .downcast_ref::() - .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::()) { - return Some( - shared - .downcast_ref::() - .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(&self) -> Option { - match self.shared_contexts.borrow().get(&TypeId::of::()) { - Some(shared) => Some( - (*shared - .downcast_ref::() - .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 + '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 + '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(&self, initializer: impl FnOnce() -> State) -> &mut State { - let mut vals = self.hook_vals.borrow_mut(); + let cur_hook = self.hook_idx.get(); + let mut hook_list = self.hook_list.borrow_mut(); - let hook_len = vals.len(); - let cur_idx = self.hook_idx.get(); - - 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::() }) diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index f448340d6..fa1036e2d 100644 --- a/packages/core/src/virtual_dom.rs +++ b/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(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::() @@ -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, ) -> 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; } diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs index c92978118..539d9b1aa 100644 --- a/packages/core/tests/task.rs +++ b/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() {