From c70e2bfcb6b0421127b1584a5d46c4232e41e039 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 5 Jan 2024 09:32:50 -0600 Subject: [PATCH] move diffing into the global runtime --- packages/core/src/any_props.rs | 50 +++++++++++++--- packages/core/src/arena.rs | 74 +---------------------- packages/core/src/diff.rs | 7 +-- packages/core/src/nodes.rs | 11 ++-- packages/core/src/scope_arena.rs | 52 +++++----------- packages/core/src/scope_context.rs | 67 +++++++++++++++++++++ packages/core/src/scopes.rs | 95 +++++------------------------- packages/core/src/virtual_dom.rs | 6 +- 8 files changed, 152 insertions(+), 210 deletions(-) diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index 420bf9d32..ff9331942 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,14 +1,40 @@ -use crate::{nodes::RenderReturn, scopes::ScopeState, Element}; -use std::panic::AssertUnwindSafe; +use crate::{nodes::RenderReturn, Element}; +use std::{ops::Deref, panic::AssertUnwindSafe}; + +/// A boxed version of AnyProps that can be cloned +pub(crate) struct BoxedAnyProps { + inner: Box, +} + +impl BoxedAnyProps { + fn new(inner: impl AnyProps + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } +} + +impl Deref for BoxedAnyProps { + type Target = dyn AnyProps; + + fn deref(&self) -> &Self::Target { + &*self.inner + } +} + +impl Clone for BoxedAnyProps { + fn clone(&self) -> Self { + Self { + inner: self.inner.duplicate(), + } + } +} /// A trait that essentially allows VComponentProps to be used generically -/// -/// # Safety -/// -/// This should not be implemented outside this module pub(crate) trait AnyProps { - fn render<'a>(&'a self, bump: &'a ScopeState) -> RenderReturn; + fn render<'a>(&'a self) -> RenderReturn; fn memoize(&self, other: &dyn AnyProps) -> bool; + fn duplicate(&self) -> Box; } pub(crate) struct VProps

{ @@ -36,7 +62,7 @@ impl AnyProps for VProps

{ (self.memo)(self, other) } - fn render(&self, cx: &ScopeState) -> RenderReturn { + fn render(&self) -> RenderReturn { let res = std::panic::catch_unwind(AssertUnwindSafe(move || { // Call the render function directly (self.render_fn)(self.props.clone()) @@ -52,4 +78,12 @@ impl AnyProps for VProps

{ } } } + + fn duplicate(&self) -> Box { + Box::new(Self { + render_fn: self.render_fn, + memo: self.memo, + props: self.props.clone(), + }) + } } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 205060b32..7ad3986ec 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -1,8 +1,8 @@ use std::ptr::NonNull; use crate::{ - innerlude::DirtyScope, nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, - AttributeValue, DynamicNode, ScopeId, + innerlude::DirtyScope, nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, DynamicNode, + ScopeId, }; /// An Element's unique identifier. @@ -48,14 +48,6 @@ impl VirtualDom { std::mem::transmute::, _>(vnode.into()) }))); - // Set this id to be dropped when the scope is rerun - if let Some(scope) = self.runtime.current_scope_id() { - self.scopes[scope.0] - .element_refs_to_drop - .borrow_mut() - .push(new_id); - } - new_id } @@ -89,17 +81,6 @@ impl VirtualDom { id, }); - // Remove all VNode ids from the scope - for id in self.scopes[id.0] - .element_refs_to_drop - .borrow_mut() - .drain(..) - { - self.element_refs.try_remove(id.0); - } - - self.ensure_drop_safety(id); - if recursive { if let Some(root) = self.scopes[id.0].try_root_node() { if let RenderReturn::Ready(node) = root { @@ -110,18 +91,6 @@ impl VirtualDom { let scope = &mut self.scopes[id.0]; - // Drop all the hooks once the children are dropped - // this means we'll drop hooks bottom-up - scope.hooks.get_mut().clear(); - { - let context = scope.context(); - - // Drop all the futures once the hooks are dropped - for task_id in context.spawned_tasks.borrow_mut().drain() { - context.tasks.remove(task_id); - } - } - self.scopes.remove(id.0); } @@ -139,45 +108,6 @@ impl VirtualDom { DynamicNode::Text(_) => {} }); } - - /// Descend through the tree, removing any borrowed props and listeners - pub(crate) fn ensure_drop_safety(&mut self, scope_id: ScopeId) { - let scope = &self.scopes[scope_id.0]; - - { - // Drop all element refs that could be invalidated when the component was rerun - let mut element_refs = self.scopes[scope_id.0].element_refs_to_drop.borrow_mut(); - let element_refs_slab = &mut self.element_refs; - for element_ref in element_refs.drain(..) { - if let Some(element_ref) = element_refs_slab.get_mut(element_ref.0) { - *element_ref = None; - } - } - } - - // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we - // run the hooks (which hold an &mut Reference) - // recursively call ensure_drop_safety on all children - let props = { scope.borrowed_props.borrow_mut().clone() }; - for comp in props { - let comp = unsafe { &*comp }; - match comp.scope.get() { - Some(child) if child != scope_id => self.ensure_drop_safety(child), - _ => (), - } - } - let scope = &self.scopes[scope_id.0]; - scope.borrowed_props.borrow_mut().clear(); - - // Now that all the references are gone, we can safely drop our own references in our listeners. - let mut listeners = scope.attributes_to_drop_before_render.borrow_mut(); - listeners.drain(..).for_each(|listener| { - let listener = unsafe { &*listener }; - if let AttributeValue::Listener(l) = &listener.value { - _ = l.take(); - } - }); - } } impl ElementPath { diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index fefb9512e..924536ef0 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -219,13 +219,12 @@ impl VirtualDom { // copy out the box for both let old_scope = &self.scopes[scope_id.0]; let old = old_scope.props.as_ref(); - let new: Box = right.props.take().unwrap(); - let new: Box = unsafe { std::mem::transmute(new) }; + let new: &dyn AnyProps = right.props.as_ref(); // If the props are static, then we try to memoize by setting the new with the old // The target scopestate still has the reference to the old props, so there's no need to update anything // This also implicitly drops the new props since they're not used - if left.static_props && unsafe { old.as_ref().unwrap().memoize(new.as_ref()) } { + if old.memoize(new) { tracing::trace!( "Memoized props for component {:#?} ({})", scope_id, @@ -235,7 +234,7 @@ impl VirtualDom { } // First, move over the props from the old to the new, dropping old props in the process - self.scopes[scope_id.0].props = Some(new); + self.scopes[scope_id.0].props = new; // Now run the component and diff it self.run_scope(scope_id); diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 67403b9ec..767b81da3 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -341,7 +341,7 @@ pub struct VComponent { /// It is possible that components get folded at compile time, so these shouldn't be really used as a key pub(crate) render_fn: *const (), - pub(crate) props: Box, + pub(crate) props: BoxedAnyProps, } impl<'a> VComponent { @@ -362,7 +362,7 @@ impl<'a> std::fmt::Debug for VComponent { } /// An instance of some text, mounted to the DOM -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct VText { /// The actual text itself pub value: String, @@ -436,7 +436,7 @@ pub enum TemplateAttribute { } /// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"` -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Attribute { /// The name of the attribute. pub name: &'static str, @@ -483,6 +483,7 @@ impl Attribute { /// /// These are built-in to be faster during the diffing process. To use a custom value, use the [`AttributeValue::Any`] /// variant. +#[derive(Clone)] pub enum AttributeValue { /// Text attribute Text(String), @@ -497,10 +498,10 @@ pub enum AttributeValue { Bool(bool), /// A listener, like "onclick" - Listener(RefCell>), + Listener(ListenerCb), /// An arbitrary value that implements PartialEq and is static - Any(RefCell>>), + Any(Box), /// A "none" value, resulting in the removal of an attribute from the dom None, diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 3666a40a5..694f9811d 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -24,15 +24,8 @@ impl VirtualDom { runtime: self.runtime.clone(), context_id: id, - props: Some(props), - - render_cnt: Default::default(), - hooks: Default::default(), - hook_idx: Default::default(), - - borrowed_props: Default::default(), - attributes_to_drop_before_render: Default::default(), - element_refs_to_drop: Default::default(), + props, + last_rendered_node: Default::default(), })); let context = @@ -42,42 +35,30 @@ impl VirtualDom { scope } - pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn { + pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> RenderReturn { self.runtime.scope_stack.borrow_mut().push(scope_id); - // Cycle to the next frame and then reset it - // This breaks any latent references, invalidating every pointer referencing into it. - // Remove all the outdated listeners - self.ensure_drop_safety(scope_id); let new_nodes = unsafe { let scope = &self.scopes[scope_id.0]; - scope.previous_frame().reset(); - scope.context().suspended.set(false); - - scope.hook_idx.set(0); + let context = scope.context(); + context.suspended.set(false); + context.hook_index.set(0); // safety: due to how we traverse the tree, we know that the scope is not currently aliased - let props: &dyn AnyProps = scope.props.as_ref().unwrap().as_ref(); - let props: &dyn AnyProps = std::mem::transmute(props); + let props: &dyn AnyProps = &*scope.props; let _span = tracing::trace_span!("render", scope = %scope.context().name); - props.render(scope) + props.render() }; - let scope = &self.scopes[scope_id.0]; - - // We write on top of the previous frame and then make it the current by pushing the generation forward - let frame = scope.previous_frame(); - - // set the new head of the bump frame - let allocated = &*frame.bump().alloc(new_nodes); - frame.node.set(allocated); - - // And move the render generation forward by one - scope.render_cnt.set(scope.render_cnt.get() + 1); + let scope = &mut self.scopes[scope_id.0]; let context = scope.context(); + + // And move the render generation forward by one + context.render_count.set(context.render_count.get() + 1); + // remove this scope from dirty scopes self.dirty_scopes.remove(&DirtyScope { height: context.height, @@ -85,18 +66,15 @@ impl VirtualDom { }); if context.suspended.get() { - if matches!(allocated, RenderReturn::Aborted(_)) { + if matches!(new_nodes, RenderReturn::Aborted(_)) { self.suspended_scopes.insert(context.id); } } else if !self.suspended_scopes.is_empty() { _ = self.suspended_scopes.remove(&context.id); } - // rebind the lifetime now that its stored internally - let result = unsafe { allocated }; - self.runtime.scope_stack.borrow_mut().pop(); - result + new_nodes } } diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index e06fe5eba..d1cece18c 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -22,10 +22,15 @@ pub(crate) struct ScopeContext { pub(crate) parent_id: Option, pub(crate) height: u32, + pub(crate) render_count: Cell, + pub(crate) suspended: Cell, pub(crate) shared_contexts: RefCell>>, + pub(crate) hooks: RefCell>>, + pub(crate) hook_index: Cell, + pub(crate) tasks: Rc, pub(crate) spawned_tasks: RefCell>, } @@ -43,10 +48,13 @@ impl ScopeContext { id, parent_id, height, + render_count: Cell::new(0), suspended: Cell::new(false), shared_contexts: RefCell::new(vec![]), tasks, spawned_tasks: RefCell::new(FxHashSet::default()), + hooks: RefCell::new(vec![]), + hook_index: Cell::new(0), } } @@ -244,6 +252,65 @@ impl ScopeContext { self.suspended.set(true); None } + + /// Store a value between renders. The foundational hook for all other hooks. + /// + /// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`. + /// + /// When the component is unmounted (removed from the UI), the value is dropped. This means you can return a custom type and provide cleanup code by implementing the [`Drop`] trait + /// + /// # Example + /// + /// ``` + /// use dioxus_core::ScopeState; + /// + /// // prints a greeting on the initial render + /// pub fn use_hello_world(cx: &ScopeState) { + /// cx.use_hook(|| println!("Hello, world!")); + /// } + /// ``` + #[allow(clippy::mut_from_ref)] + pub fn use_hook(&self, initializer: impl FnOnce() -> State) -> &mut State { + let cur_hook = self.hook_index.get(); + let mut hooks = self.hooks.try_borrow_mut().expect("The hook list is already borrowed: This error is likely caused by trying to use a hook inside a hook which violates the rules of hooks."); + + if cur_hook >= hooks.len() { + hooks.push(Box::new(initializer())); + } + + hooks + .get(cur_hook) + .and_then(|inn| { + self.hook_index.set(cur_hook + 1); + let raw_ref: &mut dyn Any = inn.as_mut(); + raw_ref.downcast_mut::() + }) + .expect( + r#" + Unable to retrieve the hook that was initialized at this index. + Consult the `rules of hooks` to understand how to use hooks properly. + + You likely used the hook in a conditional. Hooks rely on consistent ordering between renders. + Functions prefixed with "use" should never be called conditionally. + "#, + ) + } + + /// Get the current render since the inception of this component + /// + /// This can be used as a helpful diagnostic when debugging hooks/renders, etc + pub fn generation(&self) -> usize { + self.render_count.get() + } +} + +impl Drop for ScopeContext { + fn drop(&mut self) { + // Drop all spawned tasks + for id in self.spawned_tasks.borrow().iter() { + self.tasks.remove(*id); + } + } } /// Schedule an update for any component given its [`ScopeId`]. diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 980a85fe1..e26f38ca9 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,7 +1,7 @@ use crate::{ any_props::AnyProps, any_props::VProps, - innerlude::{DynamicNode, EventHandler, VComponent, VNodeId, VText}, + innerlude::{DynamicNode, EventHandler, VComponent, VText}, nodes::{IntoAttributeValue, IntoDynNode, RenderReturn}, runtime::Runtime, scope_context::ScopeContext, @@ -9,7 +9,7 @@ use crate::{ }; use std::{ any::Any, - cell::{Cell, Ref, RefCell, UnsafeCell}, + cell::{Ref, RefCell}, fmt::{Arguments, Debug}, future::Future, rc::Rc, @@ -47,16 +47,9 @@ pub struct ScopeState { pub(crate) runtime: Rc, pub(crate) context_id: ScopeId, - pub(crate) render_cnt: Cell, + pub(crate) last_rendered_node: Option, - pub(crate) hooks: RefCell>>>, - pub(crate) hook_idx: Cell, - - pub(crate) borrowed_props: RefCell>, - pub(crate) element_refs_to_drop: RefCell>, - pub(crate) attributes_to_drop_before_render: RefCell>, - - pub(crate) props: Option>, + pub(crate) props: Box, } impl Drop for ScopeState { @@ -75,13 +68,6 @@ impl<'src> ScopeState { self.context().name } - /// Get the current render since the inception of this component - /// - /// This can be used as a helpful diagnostic when debugging hooks/renders, etc - pub fn generation(&self) -> usize { - self.render_cnt.get() - } - /// 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. @@ -98,15 +84,7 @@ impl<'src> ScopeState { /// /// Returns [`None`] if the tree has not been built yet. pub fn try_root_node(&self) -> Option<&RenderReturn> { - let ptr = self.current_frame().node.get(); - - if ptr.is_null() { - return None; - } - - let r: &RenderReturn = unsafe { &*ptr }; - - unsafe { std::mem::transmute(r) } + self.last_rendered_node.as_ref() } /// Get the height of this Scope - IE the number of scopes above it. @@ -354,21 +332,19 @@ impl<'src> ScopeState { &'src self, mut callback: impl FnMut(Event) + 'src, ) -> AttributeValue { - AttributeValue::Listener(RefCell::new(Some(Box::new( - move |event: Event| { - if let Ok(data) = event.data.downcast::() { - callback(Event { - propagates: event.propagates, - data, - }); - } - }, - )))) + AttributeValue::Listener(Box::new(move |event: Event| { + if let Ok(data) = event.data.downcast::() { + callback(Event { + propagates: event.propagates, + data, + }); + } + })) } /// Create a new [`AttributeValue`] with a value that implements [`AnyValue`] pub fn any_value(&'src self, value: T) -> AttributeValue { - AttributeValue::Any(RefCell::new(Some(Box::new(value)))) + AttributeValue::Any(Box::new(value)) } /// Mark this component as suspended and then return None @@ -377,47 +353,4 @@ impl<'src> ScopeState { cx.suspend(); None } - - /// Store a value between renders. The foundational hook for all other hooks. - /// - /// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`. - /// - /// When the component is unmounted (removed from the UI), the value is dropped. This means you can return a custom type and provide cleanup code by implementing the [`Drop`] trait - /// - /// # Example - /// - /// ``` - /// use dioxus_core::ScopeState; - /// - /// // prints a greeting on the initial render - /// pub fn use_hello_world(cx: &ScopeState) { - /// cx.use_hook(|| println!("Hello, world!")); - /// } - /// ``` - #[allow(clippy::mut_from_ref)] - pub fn use_hook(&self, initializer: impl FnOnce() -> State) -> &mut State { - let cur_hook = self.hook_idx.get(); - let mut hooks = self.hooks.try_borrow_mut().expect("The hook list is already borrowed: This error is likely caused by trying to use a hook inside a hook which violates the rules of hooks."); - - if cur_hook >= hooks.len() { - hooks.push(Box::new(UnsafeCell::new(initializer()))); - } - - hooks - .get(cur_hook) - .and_then(|inn| { - self.hook_idx.set(cur_hook + 1); - let raw_ref = unsafe { &mut *inn.get() }; - raw_ref.downcast_mut::() - }) - .expect( - r#" - Unable to retrieve the hook that was initialized at this index. - Consult the `rules of hooks` to understand how to use hooks properly. - - You likely used the hook in a conditional. Hooks rely on consistent ordering between renders. - Functions prefixed with "use" should never be called conditionally. - "#, - ) - } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index a66e4aa20..5c8246355 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -224,7 +224,7 @@ impl VirtualDom { /// ``` /// /// Note: the VirtualDom is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it. - pub fn new(app: fn() -> Element) -> Self { + pub fn new(app: fn(()) -> Element) -> Self { Self::new_with_props(app, ()) } @@ -258,7 +258,7 @@ impl VirtualDom { /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" }); /// let mutations = dom.rebuild(); /// ``` - pub fn new_with_props(root: fn(P) -> Element, root_props: P) -> Self { + pub fn new_with_props(root: fn(P) -> Element, root_props: P) -> Self { let (tx, rx) = futures_channel::mpsc::unbounded(); let scheduler = Scheduler::new(tx); let mut dom = Self { @@ -450,7 +450,7 @@ impl VirtualDom { let origin = path.scope; self.runtime.scope_stack.borrow_mut().push(origin); self.runtime.rendering.set(false); - if let Some(cb) = listener.borrow_mut().as_deref_mut() { + if let Some(cb) = listener.as_deref_mut() { cb(uievent.clone()); } self.runtime.scope_stack.borrow_mut().pop();