Fix mount use after removal/Simplify mounts a bit (#2834)

* remove clone mounted; mounted nodes should never leak into components?

* Remove the mount from vnodes after unmounting them

* Fix use after removal in iterator diffing

* remove logs

* Re-fix keyed list diffing

* Use new.len for diffing keyed lens and remove redundant check

* simplify core list diffing logic a bit

* remove println

* add proper asserts for new tests

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2024-08-13 22:47:12 -07:00
1 parent 959ab67624
commit d649b0c54d
13 files changed
+310 -221

No files matched your search

+5 -7
View File
@@ -1,4 +1,4 @@
use crate::{innerlude::CapturedPanic, nodes::RenderReturn, ComponentFunction}; use crate::{innerlude::CapturedPanic, ComponentFunction, Element};
use std::{any::Any, panic::AssertUnwindSafe}; use std::{any::Any, panic::AssertUnwindSafe};
pub(crate) type BoxedAnyProps = Box<dyn AnyProps>; pub(crate) type BoxedAnyProps = Box<dyn AnyProps>;
@@ -6,7 +6,7 @@ pub(crate) type BoxedAnyProps = Box<dyn AnyProps>;
/// A trait for a component that can be rendered. /// A trait for a component that can be rendered.
pub(crate) trait AnyProps: 'static { pub(crate) trait AnyProps: 'static {
/// Render the component with the internal props. /// Render the component with the internal props.
fn render(&self) -> RenderReturn; fn render(&self) -> Element;
/// Make the old props equal to the new type erased props. Return if the props were equal and should be memoized. /// Make the old props equal to the new type erased props. Return if the props were equal and should be memoized.
fn memoize(&mut self, other: &dyn Any) -> bool; fn memoize(&mut self, other: &dyn Any) -> bool;
/// Get the props as a type erased `dyn Any`. /// Get the props as a type erased `dyn Any`.
@@ -74,20 +74,18 @@ impl<F: ComponentFunction<P, M> + Clone, P: Clone + 'static, M: 'static> AnyProp
&mut self.props &mut self.props
} }
fn render(&self) -> RenderReturn { fn render(&self) -> Element {
let res = std::panic::catch_unwind(AssertUnwindSafe(move || { let res = std::panic::catch_unwind(AssertUnwindSafe(move || {
self.render_fn.rebuild(self.props.clone()) self.render_fn.rebuild(self.props.clone())
})); }));
match res { match res {
Ok(node) => RenderReturn { node }, Ok(node) => node,
Err(err) => { Err(err) => {
let component_name = self.name; let component_name = self.name;
tracing::error!("Panic while rendering component `{component_name}`: {err:?}"); tracing::error!("Panic while rendering component `{component_name}`: {err:?}");
let panic = CapturedPanic { error: err }; let panic = CapturedPanic { error: err };
RenderReturn { Element::Err(panic.into())
node: Err(panic.into()),
}
} }
} }
} }
+16 -9
View File
@@ -9,11 +9,11 @@ use crate::{
ElementRef, MountId, ScopeOrder, SuspenseBoundaryProps, SuspenseBoundaryPropsWithOwner, ElementRef, MountId, ScopeOrder, SuspenseBoundaryProps, SuspenseBoundaryPropsWithOwner,
VComponent, WriteMutations, VComponent, WriteMutations,
}, },
nodes::VNode, nodes::{AsVNode, VNode},
prelude::SuspenseContext, prelude::SuspenseContext,
scopes::ScopeId, scopes::ScopeId,
virtual_dom::VirtualDom, virtual_dom::VirtualDom,
RenderReturn, Element,
}; };
impl VirtualDom { impl VirtualDom {
@@ -36,11 +36,11 @@ impl VirtualDom {
&mut self, &mut self,
to: Option<&mut M>, to: Option<&mut M>,
scope: ScopeId, scope: ScopeId,
new_nodes: RenderReturn, new_nodes: Element,
) { ) {
self.runtime.clone().with_scope_on_stack(scope, || { self.runtime.clone().with_scope_on_stack(scope, || {
// We don't diff the nodes if the scope is suspended or has an error // We don't diff the nodes if the scope is suspended or has an error
let Ok(new_real_nodes) = &new_nodes.node else { let Ok(new_real_nodes) = &new_nodes else {
return; return;
}; };
let scope_state = &mut self.scopes[scope.0]; let scope_state = &mut self.scopes[scope.0];
@@ -51,7 +51,8 @@ impl VirtualDom {
// If it is suspended, we need to diff it but write the mutations nothing // If it is suspended, we need to diff it but write the mutations nothing
// Note: It is important that we still diff the scope even if it is suspended, because the scope may render other child components which may change between renders // Note: It is important that we still diff the scope even if it is suspended, because the scope may render other child components which may change between renders
let mut render_to = to.filter(|_| self.runtime.scope_should_render(scope)); let mut render_to = to.filter(|_| self.runtime.scope_should_render(scope));
old.diff_node(new_real_nodes, self, render_to.as_deref_mut()); old.as_vnode()
.diff_node(new_real_nodes, self, render_to.as_deref_mut());
self.scopes[scope.0].last_rendered_node = Some(new_nodes); self.scopes[scope.0].last_rendered_node = Some(new_nodes);
@@ -69,7 +70,7 @@ impl VirtualDom {
&mut self, &mut self,
to: Option<&mut M>, to: Option<&mut M>,
scope: ScopeId, scope: ScopeId,
new_nodes: RenderReturn, new_nodes: Element,
parent: Option<ElementRef>, parent: Option<ElementRef>,
) -> usize { ) -> usize {
self.runtime.clone().with_scope_on_stack(scope, || { self.runtime.clone().with_scope_on_stack(scope, || {
@@ -79,7 +80,9 @@ impl VirtualDom {
let mut render_to = to.filter(|_| self.runtime.scope_should_render(scope)); let mut render_to = to.filter(|_| self.runtime.scope_should_render(scope));
// Create the node // Create the node
let nodes = new_nodes.create(self, parent, render_to.as_deref_mut()); let nodes = new_nodes
.as_vnode()
.create(self, parent, render_to.as_deref_mut());
// Then set the new node as the last rendered node // Then set the new node as the last rendered node
self.scopes[scope.0].last_rendered_node = Some(new_nodes); self.scopes[scope.0].last_rendered_node = Some(new_nodes);
@@ -104,8 +107,12 @@ impl VirtualDom {
// Remove the component from the dom // Remove the component from the dom
if let Some(node) = self.scopes[scope_id.0].last_rendered_node.as_ref() { if let Some(node) = self.scopes[scope_id.0].last_rendered_node.as_ref() {
node.clone_mounted() node.clone().as_vnode().remove_node_inner(
.remove_node_inner(self, to, destroy_component_state, replace_with) self,
to,
destroy_component_state,
replace_with,
)
}; };
if destroy_component_state { if destroy_component_state {
+37 -27
View File
@@ -133,29 +133,17 @@ impl VirtualDom {
let new_middle = &new[left_offset..(new.len() - right_offset)]; let new_middle = &new[left_offset..(new.len() - right_offset)];
debug_assert!( debug_assert!(
!((old_middle.len() == new_middle.len()) && old_middle.is_empty()), !old_middle.is_empty(),
"keyed children must have the same number of children" "Old middle returned from `diff_keyed_ends` should not be empty"
);
debug_assert!(
!new_middle.is_empty(),
"New middle returned from `diff_keyed_ends` should not be empty"
); );
// A few nodes in the middle were removed, just remove the old nodes
if new_middle.is_empty() { if new_middle.is_empty() {
// remove the old elements
self.remove_nodes(to, old_middle, None); self.remove_nodes(to, old_middle, None);
} else if old_middle.is_empty() {
// there were no old elements, so just create the new elements
// we need to find the right "foothold" though - we shouldn't use the "append" at all
if left_offset == 0 {
// insert at the beginning of the old list
let foothold = &old[old.len() - right_offset];
self.create_and_insert_before(to, new_middle, foothold, parent);
} else if right_offset == 0 {
// insert at the end the old list
let foothold = old.last().unwrap();
self.create_and_insert_after(to, new_middle, foothold, parent);
} else {
// inserting in the middle
let foothold = &old[left_offset - 1];
self.create_and_insert_after(to, new_middle, foothold, parent);
}
} else { } else {
self.diff_keyed_middle(to, old_middle, new_middle, parent); self.diff_keyed_middle(to, old_middle, new_middle, parent);
} }
@@ -187,14 +175,7 @@ impl VirtualDom {
// If that was all of the old children, then create and append the remaining // If that was all of the old children, then create and append the remaining
// new children and we're finished. // new children and we're finished.
if left_offset == old.len() { if left_offset == old.len() {
self.create_and_insert_after(to, &new[left_offset..], old.last().unwrap(), parent); self.create_and_insert_after(to, &new[left_offset..], &new[left_offset - 1], parent);
return None;
}
// And if that was all of the new children, then remove all of the remaining
// old children and we're finished.
if left_offset == new.len() {
self.remove_nodes(to, &old[left_offset..], None);
return None; return None;
} }
@@ -209,6 +190,35 @@ impl VirtualDom {
right_offset += 1; right_offset += 1;
} }
// If that was all of the old children, then create and prepend the remaining
// new children and we're finished.
if right_offset == old.len() {
self.create_and_insert_before(
to,
&new[..new.len() - right_offset],
&new[new.len() - right_offset],
parent,
);
return None;
}
// If the right offset + the left offset is the same as the new length, then we just need to remove the old nodes
if right_offset + left_offset == new.len() {
self.remove_nodes(to, &old[left_offset..old.len() - right_offset], None);
return None;
}
// If the right offset + the left offset is the same as the old length, then we just need to add the new nodes
if right_offset + left_offset == old.len() {
self.create_and_insert_before(
to,
&new[left_offset..new.len() - right_offset],
&new[new.len() - right_offset],
parent,
);
return None;
}
Some((left_offset, right_offset)) Some((left_offset, right_offset))
} }
+8 -7
View File
@@ -35,8 +35,6 @@ impl VNode {
return self.replace(std::slice::from_ref(new), parent, dom, to); return self.replace(std::slice::from_ref(new), parent, dom, to);
} }
let mount_id = self.mount.get();
self.move_mount_to(new, dom); self.move_mount_to(new, dom);
// If the templates are the same, we don't need to do anything, except copy over the mount information // If the templates are the same, we don't need to do anything, except copy over the mount information
@@ -52,6 +50,7 @@ impl VNode {
} }
// Now diff the dynamic nodes // Now diff the dynamic nodes
let mount_id = new.mount.get();
for (dyn_node_idx, (old, new)) in self for (dyn_node_idx, (old, new)) in self
.dynamic_nodes .dynamic_nodes
.iter() .iter()
@@ -64,7 +63,7 @@ impl VNode {
fn move_mount_to(&self, new: &VNode, dom: &mut VirtualDom) { fn move_mount_to(&self, new: &VNode, dom: &mut VirtualDom) {
// Copy over the mount information // Copy over the mount information
let mount_id = self.mount.get(); let mount_id = self.mount.take();
new.mount.set(mount_id); new.mount.set(mount_id);
if mount_id.mounted() { if mount_id.mounted() {
@@ -72,7 +71,7 @@ impl VNode {
let mount = &mut mounts[mount_id.0]; let mount = &mut mounts[mount_id.0];
// Update the reference to the node for bubbling events // Update the reference to the node for bubbling events
mount.node = new.clone_mounted(); mount.node = new.clone();
} }
} }
@@ -293,6 +292,7 @@ impl VNode {
self.reclaim_roots(mount, dom, to, destroy_component_state, replace_with); self.reclaim_roots(mount, dom, to, destroy_component_state, replace_with);
if destroy_component_state { if destroy_component_state {
let mount = self.mount.take();
// Remove the mount information // Remove the mount information
dom.runtime.mounts.borrow_mut().remove(mount.0); dom.runtime.mounts.borrow_mut().remove(mount.0);
} }
@@ -416,7 +416,7 @@ impl VNode {
dom: &mut VirtualDom, dom: &mut VirtualDom,
to: &mut impl WriteMutations, to: &mut impl WriteMutations,
) { ) {
let mount_id = self.mount.get(); let mount_id = new.mount.get();
for (idx, (old_attrs, new_attrs)) in self for (idx, (old_attrs, new_attrs)) in self
.dynamic_attrs .dynamic_attrs
.iter() .iter()
@@ -535,7 +535,7 @@ impl VNode {
self.mount.set(mount); self.mount.set(mount);
tracing::trace!(?self, ?mount, "creating template"); tracing::trace!(?self, ?mount, "creating template");
entry.insert(VNodeMount { entry.insert(VNodeMount {
node: self.clone_mounted(), node: self.clone(),
parent, parent,
root_ids: vec![ElementId(0); template.roots.len()].into_boxed_slice(), root_ids: vec![ElementId(0); template.roots.len()].into_boxed_slice(),
mounted_attributes: vec![ElementId(0); template.attr_paths.len()] mounted_attributes: vec![ElementId(0); template.attr_paths.len()]
@@ -559,7 +559,8 @@ impl VNode {
.as_usize() .as_usize()
.expect("node should already be mounted"), .expect("node should already be mounted"),
), ),
"Node mount should be valid" "Tried to find mount {:?} in dom.mounts, but it wasn't there",
self.mount.get()
); );
let mount = self.mount.get(); let mount = self.mount.get();
-11
View File
@@ -451,17 +451,6 @@ impl CapturedError {
self self
} }
/// Clone the error while retaining the mounted information of the error
pub(crate) fn clone_mounted(&self) -> Self {
Self {
error: self.error.clone(),
backtrace: self.backtrace.clone(),
scope: self.scope,
render: self.render.clone_mounted(),
context: self.context.clone(),
}
}
/// Get a VNode representation of the error if the error provides one /// Get a VNode representation of the error if the error provides one
pub fn show(&self) -> Option<Element> { pub fn show(&self) -> Option<Element> {
if self.render == VNode::placeholder() { if self.render == VNode::placeholder() {
+5 -5
View File
@@ -76,9 +76,9 @@ pub use crate::innerlude::{
fc_to_builder, generation, schedule_update, schedule_update_any, use_hook, vdom_is_rendering, fc_to_builder, generation, schedule_update, schedule_update_any, use_hook, vdom_is_rendering,
AnyValue, Attribute, AttributeValue, CapturedError, Component, ComponentFunction, DynamicNode, AnyValue, Attribute, AttributeValue, CapturedError, Component, ComponentFunction, DynamicNode,
Element, ElementId, Event, Fragment, HasAttributes, IntoDynNode, MarkerWrapper, Mutation, Element, ElementId, Event, Fragment, HasAttributes, IntoDynNode, MarkerWrapper, Mutation,
Mutations, NoOpMutations, Ok, Properties, RenderReturn, Result, Runtime, ScopeId, ScopeState, Mutations, NoOpMutations, Ok, Properties, Result, Runtime, ScopeId, ScopeState, SpawnIfAsync,
SpawnIfAsync, Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, VPlaceholder,
VPlaceholder, VText, VirtualDom, WriteMutations, VText, VirtualDom, WriteMutations,
}; };
/// The purpose of this module is to alleviate imports of many common types /// The purpose of this module is to alleviate imports of many common types
@@ -94,8 +94,8 @@ pub mod prelude {
use_hook_with_cleanup, with_owner, AnyValue, Attribute, Callback, Component, use_hook_with_cleanup, with_owner, AnyValue, Attribute, Callback, Component,
ComponentFunction, Context, Element, ErrorBoundary, ErrorContext, Event, EventHandler, ComponentFunction, Context, Element, ErrorBoundary, ErrorContext, Event, EventHandler,
Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, OptionStringFromMarker, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, OptionStringFromMarker,
Properties, ReactiveContext, RenderError, RenderReturn, Runtime, RuntimeGuard, ScopeId, Properties, ReactiveContext, RenderError, Runtime, RuntimeGuard, ScopeId, ScopeState,
ScopeState, SuperFrom, SuperInto, SuspendedFuture, SuspenseBoundary, SuspenseBoundaryProps, SuperFrom, SuperInto, SuspendedFuture, SuspenseBoundary, SuspenseBoundaryProps,
SuspenseContext, SuspenseExtension, Task, Template, TemplateAttribute, TemplateNode, VNode, SuspenseContext, SuspenseExtension, Task, Template, TemplateAttribute, TemplateNode, VNode,
VNodeInner, VirtualDom, VNodeInner, VirtualDom,
}; };
+38 -87
View File
@@ -1,4 +1,5 @@
use crate::innerlude::{RenderError, VProps}; use crate::innerlude::VProps;
use crate::prelude::RenderError;
use crate::{any_props::BoxedAnyProps, innerlude::ScopeState}; use crate::{any_props::BoxedAnyProps, innerlude::ScopeState};
use crate::{arena::ElementId, Element, Event}; use crate::{arena::ElementId, Element, Event};
use crate::{ use crate::{
@@ -6,7 +7,7 @@ use crate::{
properties::ComponentFunction, properties::ComponentFunction,
}; };
use crate::{Properties, ScopeId, VirtualDom}; use crate::{Properties, ScopeId, VirtualDom};
use std::ops::{Deref, DerefMut}; use std::ops::Deref;
use std::rc::Rc; use std::rc::Rc;
use std::vec; use std::vec;
use std::{ use std::{
@@ -15,77 +16,6 @@ use std::{
fmt::{Arguments, Debug}, fmt::{Arguments, Debug},
}; };
/// The actual state of the component's most recent computation
///
/// If the component returned early (e.g. `return None`), this will be Aborted(None)
#[derive(Debug)]
pub struct RenderReturn {
/// The node that was rendered
pub(crate) node: Element,
}
impl From<RenderReturn> for VNode {
fn from(val: RenderReturn) -> Self {
match val.node {
Ok(node) => node,
Err(RenderError::Aborted(e)) => e.render,
Err(RenderError::Suspended(fut)) => fut.placeholder,
}
}
}
impl From<Element> for RenderReturn {
fn from(node: Element) -> Self {
RenderReturn { node }
}
}
impl Clone for RenderReturn {
fn clone(&self) -> Self {
match &self.node {
Ok(node) => RenderReturn {
node: Ok(node.clone_mounted()),
},
Err(RenderError::Aborted(err)) => RenderReturn {
node: Err(RenderError::Aborted(err.clone_mounted())),
},
Err(RenderError::Suspended(fut)) => RenderReturn {
node: Err(RenderError::Suspended(fut.clone_mounted())),
},
}
}
}
impl Default for RenderReturn {
fn default() -> Self {
RenderReturn {
node: Ok(VNode::placeholder()),
}
}
}
impl Deref for RenderReturn {
type Target = VNode;
fn deref(&self) -> &Self::Target {
match &self.node {
Ok(node) => node,
Err(RenderError::Aborted(err)) => &err.render,
Err(RenderError::Suspended(fut)) => &fut.placeholder,
}
}
}
impl DerefMut for RenderReturn {
fn deref_mut(&mut self) -> &mut Self::Target {
match &mut self.node {
Ok(node) => node,
Err(RenderError::Aborted(err)) => &mut err.render,
Err(RenderError::Suspended(fut)) => &mut fut.placeholder,
}
}
}
/// The information about the /// The information about the
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct VNodeMount { pub(crate) struct VNodeMount {
@@ -162,7 +92,7 @@ pub struct VNodeInner {
/// ///
/// The dynamic parts of the template are stored separately from the static parts. This allows faster diffing by skipping /// The dynamic parts of the template are stored separately from the static parts. This allows faster diffing by skipping
/// static parts of the template. /// static parts of the template.
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct VNode { pub struct VNode {
vnode: Rc<VNodeInner>, vnode: Rc<VNodeInner>,
@@ -170,15 +100,44 @@ pub struct VNode {
pub(crate) mount: Cell<MountId>, pub(crate) mount: Cell<MountId>,
} }
impl Clone for VNode { impl AsRef<VNode> for Element {
fn clone(&self) -> Self { fn as_ref(&self) -> &VNode {
Self { match self {
vnode: self.vnode.clone(), Element::Ok(node) => node,
mount: Default::default(), Element::Err(RenderError::Aborted(err)) => &err.render,
Element::Err(RenderError::Suspended(fut)) => &fut.placeholder,
} }
} }
} }
impl From<&Element> for VNode {
fn from(val: &Element) -> Self {
AsRef::as_ref(val).clone()
}
}
impl From<Element> for VNode {
fn from(val: Element) -> Self {
match val {
Element::Ok(node) => node,
Element::Err(RenderError::Aborted(err)) => err.render,
Element::Err(RenderError::Suspended(fut)) => fut.placeholder,
}
}
}
/// A tiny helper trait to get the vnode for a Element
pub(crate) trait AsVNode {
/// Get the vnode for this element
fn as_vnode(&self) -> &VNode;
}
impl AsVNode for Element {
fn as_vnode(&self) -> &VNode {
AsRef::as_ref(self)
}
}
impl Default for VNode { impl Default for VNode {
fn default() -> Self { fn default() -> Self {
Self::placeholder() Self::placeholder()
@@ -223,14 +182,6 @@ impl Deref for VNode {
} }
impl VNode { impl VNode {
/// Clone the element while retaining the mount information of the node
pub(crate) fn clone_mounted(&self) -> Self {
Self {
vnode: self.vnode.clone(),
mount: self.mount.clone(),
}
}
/// Create a template with no nodes that will be skipped over during diffing /// Create a template with no nodes that will be skipped over during diffing
pub fn empty() -> Element { pub fn empty() -> Element {
Ok(Self::default()) Ok(Self::default())
+4 -8
View File
@@ -1,4 +1,4 @@
use crate::innerlude::{throw_error, RenderError, RenderReturn, ScopeOrder}; use crate::innerlude::{throw_error, RenderError, ScopeOrder};
use crate::prelude::ReactiveContext; use crate::prelude::ReactiveContext;
use crate::scope_context::SuspenseLocation; use crate::scope_context::SuspenseLocation;
use crate::{ use crate::{
@@ -48,7 +48,7 @@ impl VirtualDom {
/// Run a scope and return the rendered nodes. This will not modify the DOM or update the last rendered node of the scope. /// Run a scope and return the rendered nodes. This will not modify the DOM or update the last rendered node of the scope.
#[tracing::instrument(skip(self), level = "trace", name = "VirtualDom::run_scope")] #[tracing::instrument(skip(self), level = "trace", name = "VirtualDom::run_scope")]
#[track_caller] #[track_caller]
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> RenderReturn { pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> Element {
// Ensure we are currently inside a `Runtime`. // Ensure we are currently inside a `Runtime`.
crate::Runtime::current().unwrap_or_else(|e| panic!("{}", e)); crate::Runtime::current().unwrap_or_else(|e| panic!("{}", e));
@@ -70,11 +70,7 @@ impl VirtualDom {
span.in_scope(|| { span.in_scope(|| {
scope.reactive_context.reset_and_run_in(|| { scope.reactive_context.reset_and_run_in(|| {
let mut render_return = props.render(); let mut render_return = props.render();
self.handle_element_return( self.handle_element_return(&mut render_return, scope_id, &scope.state());
&mut render_return.node,
scope_id,
&scope.state(),
);
render_return render_return
}) })
}) })
@@ -102,7 +98,7 @@ impl VirtualDom {
"Error while rendering component `{}`:\n{e}", "Error while rendering component `{}`:\n{e}",
scope_state.name scope_state.name
); );
throw_error(e.clone_mounted()); throw_error(e.clone());
e.render = VNode::placeholder(); e.render = VNode::placeholder();
} }
Err(RenderError::Suspended(e)) => { Err(RenderError::Suspended(e)) => {
+4 -4
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
any_props::BoxedAnyProps, nodes::RenderReturn, reactive_context::ReactiveContext, any_props::BoxedAnyProps, nodes::AsVNode, reactive_context::ReactiveContext,
scope_context::Scope, Runtime, VNode, scope_context::Scope, Element, Runtime, VNode,
}; };
use std::{cell::Ref, rc::Rc}; use std::{cell::Ref, rc::Rc};
@@ -70,7 +70,7 @@ pub struct ScopeState {
pub(crate) context_id: ScopeId, pub(crate) context_id: ScopeId,
/// The last node that has been rendered for this component. This node may not ben mounted /// The last node that has been rendered for this component. This node may not ben mounted
/// During suspense, this component can be rendered in the background multiple times /// During suspense, this component can be rendered in the background multiple times
pub(crate) last_rendered_node: Option<RenderReturn>, pub(crate) last_rendered_node: Option<Element>,
pub(crate) props: BoxedAnyProps, pub(crate) props: BoxedAnyProps,
pub(crate) reactive_context: ReactiveContext, pub(crate) reactive_context: ReactiveContext,
} }
@@ -98,7 +98,7 @@ impl ScopeState {
/// ///
/// Returns [`None`] if the tree has not been built yet. /// Returns [`None`] if the tree has not been built yet.
pub fn try_root_node(&self) -> Option<&VNode> { pub fn try_root_node(&self) -> Option<&VNode> {
self.last_rendered_node.as_deref() self.last_rendered_node.as_ref().map(AsVNode::as_vnode)
} }
/// Returns the scope id of this [`ScopeState`]. /// Returns the scope id of this [`ScopeState`].
+38 -46
View File
@@ -306,23 +306,17 @@ impl SuspenseBoundaryProps {
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
.unwrap(); .unwrap();
let children = RenderReturn { let children = props.children.clone();
node: props
.children
.as_ref()
.map(|node| node.clone_mounted())
.map_err(Clone::clone),
};
// First always render the children in the background. Rendering the children may cause this boundary to suspend // First always render the children in the background. Rendering the children may cause this boundary to suspend
suspense_context.under_suspense_boundary(&dom.runtime(), || { suspense_context.under_suspense_boundary(&dom.runtime(), || {
children.create(dom, parent, None::<&mut M>); children.as_vnode().create(dom, parent, None::<&mut M>);
}); });
// Store the (now mounted) children back into the scope state // Store the (now mounted) children back into the scope state
let scope_state = &mut dom.scopes[scope_id.0]; let scope_state = &mut dom.scopes[scope_id.0];
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
props.children = children.clone().node; props.children = children.clone();
let scope_state = &mut dom.scopes[scope_id.0]; let scope_state = &mut dom.scopes[scope_id.0];
let suspense_context = scope_state let suspense_context = scope_state
@@ -345,11 +339,8 @@ impl SuspenseBoundaryProps {
.unwrap(); .unwrap();
suspense_context.set_suspended_nodes(children.into()); suspense_context.set_suspended_nodes(children.into());
let suspense_placeholder = props.fallback.call(suspense_context); let suspense_placeholder = props.fallback.call(suspense_context);
let node = RenderReturn { let nodes_created = suspense_placeholder.as_vnode().create(dom, parent, to);
node: suspense_placeholder, (suspense_placeholder, nodes_created)
};
let nodes_created = node.create(dom, parent, to);
(node, nodes_created)
}); });
let scope_state = &mut dom.scopes[scope_id.0]; let scope_state = &mut dom.scopes[scope_id.0];
@@ -358,9 +349,11 @@ impl SuspenseBoundaryProps {
nodes_created nodes_created
} else { } else {
// Otherwise just render the children in the real dom // Otherwise just render the children in the real dom
debug_assert!(children.mount.get().mounted()); debug_assert!(children.as_vnode().mount.get().mounted());
let nodes_created = suspense_context let nodes_created = suspense_context
.under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to)); .under_suspense_boundary(&dom.runtime(), || {
children.as_vnode().create(dom, parent, to)
});
let scope_state = &mut dom.scopes[scope_id.0]; let scope_state = &mut dom.scopes[scope_id.0];
scope_state.last_rendered_node = Some(children); scope_state.last_rendered_node = Some(children);
let suspense_context = let suspense_context =
@@ -403,7 +396,7 @@ impl SuspenseBoundaryProps {
// Get the parent of the suspense boundary to later create children with the right parent // Get the parent of the suspense boundary to later create children with the right parent
let currently_rendered = scope_state.last_rendered_node.as_ref().unwrap().clone(); let currently_rendered = scope_state.last_rendered_node.as_ref().unwrap().clone();
let mount = currently_rendered.mount.get(); let mount = currently_rendered.as_vnode().mount.get();
let parent = { let parent = {
let mounts = dom.runtime.mounts.borrow(); let mounts = dom.runtime.mounts.borrow();
mounts mounts
@@ -415,11 +408,7 @@ impl SuspenseBoundaryProps {
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
// Unmount any children to reset any scopes under this suspense boundary // Unmount any children to reset any scopes under this suspense boundary
let children = props let children = props.children.clone();
.children
.as_ref()
.map(|node| node.clone_mounted())
.map_err(Clone::clone);
let suspense_context = let suspense_context =
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
.unwrap(); .unwrap();
@@ -429,23 +418,24 @@ impl SuspenseBoundaryProps {
node.remove_node(&mut *dom, None::<&mut M>, None); node.remove_node(&mut *dom, None::<&mut M>, None);
} }
// Replace the rendered nodes with resolved nodes // Replace the rendered nodes with resolved nodes
currently_rendered.remove_node(&mut *dom, Some(to), Some(replace_with)); currently_rendered
.as_vnode()
.remove_node(&mut *dom, Some(to), Some(replace_with));
// Switch to only writing templates // Switch to only writing templates
only_write_templates(to); only_write_templates(to);
let children = RenderReturn { node: children }; children.as_vnode().mount.take();
children.mount.take();
// First always render the children in the background. Rendering the children may cause this boundary to suspend // First always render the children in the background. Rendering the children may cause this boundary to suspend
suspense_context.under_suspense_boundary(&dom.runtime(), || { suspense_context.under_suspense_boundary(&dom.runtime(), || {
children.create(dom, parent, Some(to)); children.as_vnode().create(dom, parent, Some(to));
}); });
// Store the (now mounted) children back into the scope state // Store the (now mounted) children back into the scope state
let scope_state = &mut dom.scopes[scope_id.0]; let scope_state = &mut dom.scopes[scope_id.0];
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
props.children = children.clone().node; props.children = children.clone();
scope_state.last_rendered_node = Some(children); scope_state.last_rendered_node = Some(children);
}) })
} }
@@ -461,7 +451,7 @@ impl SuspenseBoundaryProps {
.unwrap() .unwrap()
.clone(); .clone();
let last_rendered_node = scope.last_rendered_node.as_ref().unwrap().clone_mounted(); let last_rendered_node = scope.last_rendered_node.as_ref().unwrap().clone();
let Self { let Self {
fallback, children, .. fallback, children, ..
@@ -474,17 +464,19 @@ impl SuspenseBoundaryProps {
// We already have suspended nodes that still need to be suspended // We already have suspended nodes that still need to be suspended
// Just diff the normal and suspended nodes // Just diff the normal and suspended nodes
(Some(suspended_nodes), true) => { (Some(suspended_nodes), true) => {
let new_suspended_nodes: VNode = RenderReturn { node: children }.into(); let new_suspended_nodes: VNode = children.into();
// Diff the placeholder nodes in the dom // Diff the placeholder nodes in the dom
let new_placeholder = let new_placeholder =
suspense_context.in_suspense_placeholder(&dom.runtime(), || { suspense_context.in_suspense_placeholder(&dom.runtime(), || {
let old_placeholder = last_rendered_node; let old_placeholder = last_rendered_node;
let new_placeholder = RenderReturn { let new_placeholder = fallback.call(suspense_context.clone());
node: fallback.call(suspense_context.clone()),
};
old_placeholder.diff_node(&new_placeholder, dom, to); old_placeholder.as_vnode().diff_node(
new_placeholder.as_vnode(),
dom,
to,
);
new_placeholder new_placeholder
}); });
@@ -506,10 +498,12 @@ impl SuspenseBoundaryProps {
// We have no suspended nodes, and we are not suspended. Just diff the children like normal // We have no suspended nodes, and we are not suspended. Just diff the children like normal
(None, false) => { (None, false) => {
let old_children = last_rendered_node; let old_children = last_rendered_node;
let new_children = RenderReturn { node: children }; let new_children = children;
suspense_context.under_suspense_boundary(&dom.runtime(), || { suspense_context.under_suspense_boundary(&dom.runtime(), || {
old_children.diff_node(&new_children, dom, to); old_children
.as_vnode()
.diff_node(new_children.as_vnode(), dom, to);
}); });
// Set the last rendered node to the new children // Set the last rendered node to the new children
@@ -517,12 +511,10 @@ impl SuspenseBoundaryProps {
} }
// We have no suspended nodes, but we just became suspended. Move the children to the background // We have no suspended nodes, but we just became suspended. Move the children to the background
(None, true) => { (None, true) => {
let old_children = last_rendered_node; let old_children = last_rendered_node.as_vnode();
let new_children: VNode = RenderReturn { node: children }.into(); let new_children: VNode = children.into();
let new_placeholder = RenderReturn { let new_placeholder = fallback.call(suspense_context.clone());
node: fallback.call(suspense_context.clone()),
};
// Move the children to the background // Move the children to the background
let mount = old_children.mount.get(); let mount = old_children.mount.get();
@@ -530,7 +522,7 @@ impl SuspenseBoundaryProps {
suspense_context.in_suspense_placeholder(&dom.runtime(), || { suspense_context.in_suspense_placeholder(&dom.runtime(), || {
old_children.move_node_to_background( old_children.move_node_to_background(
std::slice::from_ref(&*new_placeholder), std::slice::from_ref(new_placeholder.as_vnode()),
parent, parent,
dom, dom,
to, to,
@@ -559,17 +551,17 @@ impl SuspenseBoundaryProps {
// Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing // Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing
let old_suspended_nodes = suspense_context.take_suspended_nodes().unwrap(); let old_suspended_nodes = suspense_context.take_suspended_nodes().unwrap();
let old_placeholder = last_rendered_node; let old_placeholder = last_rendered_node;
let new_children = RenderReturn { node: children }; let new_children = children;
// First diff the two children nodes in the background // First diff the two children nodes in the background
suspense_context.under_suspense_boundary(&dom.runtime(), || { suspense_context.under_suspense_boundary(&dom.runtime(), || {
old_suspended_nodes.diff_node(&new_children, dom, None::<&mut M>); old_suspended_nodes.diff_node(new_children.as_vnode(), dom, None::<&mut M>);
// Then replace the placeholder with the new children // Then replace the placeholder with the new children
let mount = old_placeholder.mount.get(); let mount = old_placeholder.as_vnode().mount.get();
let parent = dom.get_mounted_parent(mount); let parent = dom.get_mounted_parent(mount);
old_placeholder.replace( old_placeholder.as_vnode().replace(
std::slice::from_ref(&*new_children), std::slice::from_ref(new_children.as_vnode()),
parent, parent,
dom, dom,
to, to,
+1 -10
View File
@@ -70,15 +70,6 @@ impl SuspendedFuture {
pub fn task(&self) -> Task { pub fn task(&self) -> Task {
self.task self.task
} }
/// Clone the future while retaining the mounted information of the future
pub(crate) fn clone_mounted(&self) -> Self {
Self {
task: self.task,
origin: self.origin,
placeholder: self.placeholder.clone_mounted(),
}
}
} }
/// A context with information about suspended components /// A context with information about suspended components
@@ -117,7 +108,7 @@ impl SuspenseContext {
.suspended_nodes .suspended_nodes
.borrow() .borrow()
.as_ref() .as_ref()
.map(|node| node.clone_mounted()) .map(|node| node.clone())
} }
/// Set the suspense boundary's suspended nodes /// Set the suspense boundary's suspended nodes
+65
View File
@@ -47,3 +47,68 @@ fn toggle_option_text() {
] ]
); );
} }
// Regression test for https://github.com/DioxusLabs/dioxus/issues/2815
#[test]
fn toggle_template() {
fn app() -> Element {
rsx!(
Comp {
if true {
"{true}"
}
}
)
}
#[component]
fn Comp(children: Element) -> Element {
let show = generation() % 2 == 0;
rsx! {
if show {
{children}
}
}
}
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
// Rendering again should replace the placeholder with an text node
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().edits,
[
CreatePlaceholder { id: ElementId(2) },
ReplaceWith { id: ElementId(1), m: 1 },
]
);
dom.mark_dirty(ScopeId(ScopeId::APP.0 + 1));
assert_eq!(
dom.render_immediate_to_vec().edits,
[
CreateTextNode { value: "true".to_string(), id: ElementId(1) },
ReplaceWith { id: ElementId(2), m: 1 },
]
);
dom.mark_dirty(ScopeId(ScopeId::APP.0 + 1));
assert_eq!(
dom.render_immediate_to_vec().edits,
[
CreatePlaceholder { id: ElementId(2) },
ReplaceWith { id: ElementId(1), m: 1 },
]
);
dom.mark_dirty(ScopeId(ScopeId::APP.0 + 1));
assert_eq!(
dom.render_immediate_to_vec().edits,
[
CreateTextNode { value: "true".to_string(), id: ElementId(1) },
ReplaceWith { id: ElementId(2), m: 1 },
]
);
}
+89
View File
@@ -356,3 +356,92 @@ fn no_common_keys() {
] ]
); );
} }
#[test]
fn perfect_reverse() {
let mut dom = VirtualDom::new(|| {
let order: &[_] = match generation() % 2 {
0 => &[1, 2, 3, 4, 5, 6, 7, 8],
1 => &[9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
_ => unreachable!(),
};
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::APP);
let edits = dom.render_immediate_to_vec().edits;
assert_eq!(
edits,
[
LoadTemplate { index: 0, id: ElementId(9,) },
InsertAfter { id: ElementId(1,), m: 1 },
LoadTemplate { index: 0, id: ElementId(10,) },
PushRoot { id: ElementId(8,) },
PushRoot { id: ElementId(7,) },
PushRoot { id: ElementId(6,) },
PushRoot { id: ElementId(5,) },
PushRoot { id: ElementId(4,) },
PushRoot { id: ElementId(3,) },
PushRoot { id: ElementId(2,) },
InsertBefore { id: ElementId(1,), m: 8 },
]
)
}
#[test]
fn old_middle_empty_left_pivot() {
let mut dom = VirtualDom::new(|| {
let order: &[_] = match generation() % 2 {
0 => &[/* */ /* */ 6, 7, 8, 9, 10],
1 => &[/* */ 4, 5, /* */ 6, 7, 8, 9, 10],
_ => unreachable!(),
};
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::APP);
let edits = dom.render_immediate_to_vec().edits;
assert_eq!(
edits,
[
LoadTemplate { index: 0, id: ElementId(6,) },
LoadTemplate { index: 0, id: ElementId(7,) },
InsertBefore { id: ElementId(1,), m: 2 },
]
)
}
#[test]
fn old_middle_empty_right_pivot() {
let mut dom = VirtualDom::new(|| {
let order: &[_] = match generation() % 2 {
0 => &[1, 2, 3, /* */ 6, 7, 8, 9, 10],
1 => &[1, 2, 3, /* */ 4, 5, 6, 7, 8, 9, 10 /* */],
// 0 => &[/* */ 6, 7, 8, 9, 10],
// 1 => &[/* */ 6, 7, 8, 9, 10, /* */ 4, 5],
_ => unreachable!(),
};
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::APP);
let edits = dom.render_immediate_to_vec().edits;
assert_eq!(
edits,
[
LoadTemplate { index: 0, id: ElementId(9) },
LoadTemplate { index: 0, id: ElementId(10) },
InsertBefore { id: ElementId(4), m: 2 },
]
);
}