move diffing into the global runtime

This commit is contained in:
Evan Almloff 2024-01-05 09:32:50 -06:00
parent f42ef3ef9d
commit c70e2bfcb6
8 changed files with 152 additions and 210 deletions

View file

@ -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<dyn AnyProps>,
}
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<dyn AnyProps>;
}
pub(crate) struct VProps<P> {
@ -36,7 +62,7 @@ impl<P: Clone> AnyProps for VProps<P> {
(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<P: Clone> AnyProps for VProps<P> {
}
}
}
fn duplicate(&self) -> Box<dyn AnyProps> {
Box::new(Self {
render_fn: self.render_fn,
memo: self.memo,
props: self.props.clone(),
})
}
}

View file

@ -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::<NonNull<VNode>, _>(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 {

View file

@ -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<dyn AnyProps> = right.props.take().unwrap();
let new: Box<dyn AnyProps> = 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);

View file

@ -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<dyn AnyProps>,
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<Option<ListenerCb>>),
Listener(ListenerCb),
/// An arbitrary value that implements PartialEq and is static
Any(RefCell<Option<Box<dyn AnyValue>>>),
Any(Box<dyn AnyValue>),
/// A "none" value, resulting in the removal of an attribute from the dom
None,

View file

@ -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);
// 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 _span = tracing::trace_span!("render", scope = %scope.context().name);
props.render(scope)
};
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 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;
let _span = tracing::trace_span!("render", scope = %scope.context().name);
props.render()
};
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
}
}

View file

@ -22,10 +22,15 @@ pub(crate) struct ScopeContext {
pub(crate) parent_id: Option<ScopeId>,
pub(crate) height: u32,
pub(crate) render_count: Cell<usize>,
pub(crate) suspended: Cell<bool>,
pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>,
pub(crate) hooks: RefCell<Vec<Box<dyn Any>>>,
pub(crate) hook_index: Cell<usize>,
pub(crate) tasks: Rc<Scheduler>,
pub(crate) spawned_tasks: RefCell<FxHashSet<TaskId>>,
}
@ -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<State: 'static>(&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::<State>()
})
.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`].

View file

@ -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<Runtime>,
pub(crate) context_id: ScopeId,
pub(crate) render_cnt: Cell<usize>,
pub(crate) last_rendered_node: Option<RenderReturn>,
pub(crate) hooks: RefCell<Vec<Box<UnsafeCell<dyn Any>>>>,
pub(crate) hook_idx: Cell<usize>,
pub(crate) borrowed_props: RefCell<Vec<*const VComponent>>,
pub(crate) element_refs_to_drop: RefCell<Vec<VNodeId>>,
pub(crate) attributes_to_drop_before_render: RefCell<Vec<*const Attribute>>,
pub(crate) props: Option<Box<dyn AnyProps>>,
pub(crate) props: Box<dyn AnyProps>,
}
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<T>) + 'src,
) -> AttributeValue {
AttributeValue::Listener(RefCell::new(Some(Box::new(
move |event: Event<dyn Any>| {
AttributeValue::Listener(Box::new(move |event: Event<dyn Any>| {
if let Ok(data) = event.data.downcast::<T>() {
callback(Event {
propagates: event.propagates,
data,
});
}
},
))))
}))
}
/// Create a new [`AttributeValue`] with a value that implements [`AnyValue`]
pub fn any_value<T: AnyValue>(&'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<State: 'static>(&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::<State>()
})
.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.
"#,
)
}
}

View file

@ -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<P: 'static>(root: fn(P) -> Element, root_props: P) -> Self {
pub fn new_with_props<P: Clone + 'static>(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();