Merge remote-tracking branch 'upstream/master' into optimize-templates-v3

This commit is contained in:
Evan Almloff committed 2022-12-07 17:20:31 -06:00
commit b79ad4f50e
68 files changed
+1030 -1281

No files matched your search

+59 -34
View File
@@ -1,7 +1,4 @@
use crate::{
nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, AttributeValue, DynamicNode,
ScopeId,
};
use crate::{nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, DynamicNode, ScopeId};
use bumpalo::boxed::Box as BumpBox;
/// An Element's unique identifier.
@@ -37,21 +34,20 @@ impl ElementRef {
impl VirtualDom {
pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
let entry = self.elements.vacant_entry();
let id = entry.key();
entry.insert(ElementRef {
template: template as *const _ as *mut _,
path: ElementPath::Deep(path),
});
ElementId(id)
self.next(template, ElementPath::Deep(path))
}
pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
self.next(template, ElementPath::Root(path))
}
fn next(&mut self, template: &VNode, path: ElementPath) -> ElementId {
let entry = self.elements.vacant_entry();
let id = entry.key();
entry.insert(ElementRef {
template: template as *const _ as *mut _,
path: ElementPath::Root(path),
path,
});
ElementId(id)
}
@@ -64,9 +60,9 @@ impl VirtualDom {
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<ElementRef> {
if el.0 == 0 {
panic!(
"Invalid element set to 0 - {:#?}",
"Cannot reclaim the root element - {:#?}",
std::backtrace::Backtrace::force_capture()
)
);
}
self.elements.try_remove(el.0)
@@ -79,17 +75,15 @@ impl VirtualDom {
// Drop a scope and all its children
pub(crate) fn drop_scope(&mut self, id: ScopeId) {
let scope = self.scopes.get(id.0).unwrap();
if let Some(root) = scope.as_ref().try_root_node() {
let root = unsafe { root.extend_lifetime_ref() };
if let RenderReturn::Sync(Ok(node)) = root {
if let Some(root) = self.scopes[id.0].as_ref().try_root_node() {
if let RenderReturn::Sync(Ok(node)) = unsafe { root.extend_lifetime_ref() } {
self.drop_scope_inner(node)
}
}
let scope = self.scopes.get_mut(id.0).unwrap();
scope.props.take();
self.scopes[id.0].props.take();
let scope = &mut self.scopes[id.0];
// Drop all the hooks once the children are dropped
// this means we'll drop hooks bottom-up
@@ -99,24 +93,55 @@ impl VirtualDom {
}
fn drop_scope_inner(&mut self, node: &VNode) {
for attr in node.dynamic_attrs {
if let AttributeValue::Listener(l) = &attr.value {
l.borrow_mut().take();
node.clear_listeners();
node.dynamic_nodes.iter().for_each(|node| match node {
DynamicNode::Component(c) => self.drop_scope(c.scope.get().unwrap()),
DynamicNode::Fragment(nodes) => {
nodes.iter().for_each(|node| self.drop_scope_inner(node))
}
}
DynamicNode::Placeholder(t) => {
self.try_reclaim(t.get());
}
DynamicNode::Text(t) => {
self.try_reclaim(t.id.get());
}
});
for (idx, _) in node.template.roots.iter().enumerate() {
match node.dynamic_root(idx) {
Some(DynamicNode::Component(c)) => self.drop_scope(c.scope.get().unwrap()),
Some(DynamicNode::Fragment(nodes)) => {
for node in *nodes {
self.drop_scope_inner(node);
}
}
_ => {}
for root in node.root_ids {
let id = root.get();
if id.0 != 0 {
self.try_reclaim(id);
}
}
}
/// Descend through the tree, removing any borrowed props and listeners
pub(crate) fn ensure_drop_safety(&self, scope: ScopeId) {
let node = unsafe { self.scopes[scope.0].previous_frame().try_load_node() };
// And now we want to make sure the previous frame has dropped anything that borrows self
if let Some(RenderReturn::Sync(Ok(node))) = node {
self.ensure_drop_safety_inner(node);
}
}
fn ensure_drop_safety_inner(&self, node: &VNode) {
node.clear_listeners();
node.dynamic_nodes.iter().for_each(|child| match child {
// Only descend if the props are borrowed
DynamicNode::Component(c) if !c.static_props => {
self.ensure_drop_safety(c.scope.get().unwrap());
c.props.set(None);
}
DynamicNode::Fragment(f) => f
.iter()
.for_each(|node| self.ensure_drop_safety_inner(node)),
_ => {}
});
}
}
impl ElementPath {
+8 -6
View File
@@ -1,4 +1,5 @@
use std::cell::Cell;
use std::rc::Rc;
use crate::innerlude::{VComponent, VText};
use crate::mutations::Mutation;
@@ -42,7 +43,7 @@ impl<'b> VirtualDom {
for (root_idx, root) in template.template.roots.iter().enumerate() {
// We might need to generate an ID for the root node
on_stack += match root {
TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => {
TemplateNode::DynamicText { id } | TemplateNode::Dynamic { id } => {
match &template.dynamic_nodes[*id] {
// a dynamic text node doesn't replace a template node, instead we create it on the fly
DynamicNode::Text(VText { id: slot, value }) => {
@@ -72,7 +73,7 @@ impl<'b> VirtualDom {
}
}
TemplateNode::Element { .. } | TemplateNode::Text(_) => {
TemplateNode::Element { .. } | TemplateNode::Text { .. } => {
let this_id = self.next_root(template, root_idx);
template.root_ids[root_idx].set(this_id);
@@ -213,7 +214,7 @@ impl<'b> VirtualDom {
if template.template.roots.iter().all(|root| {
matches!(
root,
TemplateNode::Dynamic(_) | TemplateNode::DynamicText(_)
TemplateNode::Dynamic { .. } | TemplateNode::DynamicText { .. }
)
}) {
return;
@@ -302,7 +303,8 @@ impl<'b> VirtualDom {
let unbounded_props = unsafe { std::mem::transmute(props) };
let scope = self.new_scope(unbounded_props).id;
let scope = self.new_scope(unbounded_props, component.name);
let scope = scope.id;
component.scope.set(Some(scope));
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
@@ -336,8 +338,8 @@ impl<'b> VirtualDom {
}
// If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense
let boundary = match self.scopes[scope.0].has_context::<SuspenseContext>() {
Some(boundary) => unsafe { &*(boundary as *const SuspenseContext) },
let boundary = match self.scopes[scope.0].has_context::<Rc<SuspenseContext>>() {
Some(boundary) => boundary,
_ => return created,
};
+30 -32
View File
@@ -25,40 +25,35 @@ impl<'b> VirtualDom {
.previous_frame()
.try_load_node()
.expect("Call rebuild before diffing");
let new = scope_state
.current_frame()
.try_load_node()
.expect("Call rebuild before diffing");
self.diff_maybe_node(old, new);
use RenderReturn::{Async, Sync};
match (old, new) {
(Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r),
// Err cases
(Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e),
(Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r),
(Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ }
// Async
(Sync(Ok(_l)), Async(_)) => todo!(),
(Sync(Err(_e)), Async(_)) => todo!(),
(Async(_), Sync(Ok(_r))) => todo!(),
(Async(_), Sync(Err(_e))) => { /* nothing */ }
(Async(_), Async(_)) => { /* nothing */ }
};
}
self.scope_stack.pop();
}
fn diff_maybe_node(&mut self, old: &'b RenderReturn<'b>, new: &'b RenderReturn<'b>) {
use RenderReturn::{Async, Sync};
match (old, new) {
(Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r),
// Err cases
(Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e),
(Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r),
(Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ }
// Async
(Sync(Ok(_l)), Async(_)) => todo!(),
(Sync(Err(_e)), Async(_)) => todo!(),
(Async(_), Sync(Ok(_r))) => todo!(),
(Async(_), Sync(Err(_e))) => { /* nothing */ }
(Async(_), Async(_)) => { /* nothing */ }
}
}
fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {
todo!("Not yet handling error rollover")
}
fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {
todo!("Not yet handling error rollover")
}
fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {}
fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {}
fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
if !std::ptr::eq(left_template.template.name, right_template.template.name)
@@ -144,16 +139,18 @@ impl<'b> VirtualDom {
}
fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell<ElementId>) {
// Remove the old nodes, except for one
self.remove_nodes(&l[1..]);
// Now create the new one
let first = self.replace_inner(&l[0]);
// Create the placeholder first, ensuring we get a dedicated ID for the placeholder
let placeholder = self.next_element(&l[0], &[]);
r.set(placeholder);
self.mutations
.push(Mutation::CreatePlaceholder { id: placeholder });
// Remove the old nodes, except for onea
let first = self.replace_inner(&l[0]);
self.remove_nodes(&l[1..]);
self.mutations
.push(Mutation::ReplaceWith { id: first, m: 1 });
@@ -746,7 +743,8 @@ impl<'b> VirtualDom {
/// Remove these nodes from the dom
/// Wont generate mutations for the inner nodes
fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) {
nodes.iter().for_each(|node| self.remove_node(node));
// note that we iterate in reverse to unlink lists of nodes in their rough index order
nodes.iter().rev().for_each(|node| self.remove_node(node));
}
fn remove_node(&mut self, node: &'b VNode<'b>) {
@@ -911,7 +909,7 @@ fn matching_components<'a>(
.zip(right.template.roots.iter())
.map(|(l, r)| {
let (l, r) = match (l, r) {
(TemplateNode::Dynamic(l), TemplateNode::Dynamic(r)) => (l, r),
(TemplateNode::Dynamic { id: l }, TemplateNode::Dynamic { id: r }) => (l, r),
_ => return None,
};
+2 -2
View File
@@ -29,8 +29,8 @@ use crate::innerlude::*;
pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
let children = cx.props.0.as_ref().map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(VNode {
key: children.key.clone(),
parent: children.parent.clone(),
key: children.key,
parent: children.parent,
template: children.template,
root_ids: children.root_ids,
dynamic_nodes: children.dynamic_nodes,
+1
View File
@@ -13,6 +13,7 @@ use crate::{arena::ElementId, ScopeId, Template};
/// Templates, however, apply to all subtrees, not just target subtree.
///
/// Mutations are the only link between the RealDOM and the VirtualDOM.
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[derive(Debug, Default)]
#[must_use = "not handling edits can lead to visual inconsistencies in UI"]
pub struct Mutations<'a> {
+34 -8
View File
@@ -78,12 +78,20 @@ impl<'a> VNode<'a> {
/// Returns [`None`] if the root is actually a static node (Element/Text)
pub fn dynamic_root(&self, idx: usize) -> Option<&'a DynamicNode<'a>> {
match &self.template.roots[idx] {
TemplateNode::Element { .. } | TemplateNode::Text(_) => None,
TemplateNode::Dynamic(id) | TemplateNode::DynamicText(id) => {
TemplateNode::Element { .. } | TemplateNode::Text { text: _ } => None,
TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => {
Some(&self.dynamic_nodes[*id])
}
}
}
pub(crate) fn clear_listeners(&self) {
for attr in self.dynamic_attrs {
if let AttributeValue::Listener(l) = &attr.value {
l.borrow_mut().take();
}
}
}
}
/// A static layout of a UI tree that describes a set of dynamic and static nodes.
@@ -124,7 +132,7 @@ pub struct Template<'a> {
///
/// This can be created at compile time, saving the VirtualDom time when diffing the tree
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "serialize", derive(serde::Serialize), serde(tag = "type"))]
pub enum TemplateNode<'a> {
/// An statically known element in the dom.
///
@@ -151,15 +159,24 @@ pub enum TemplateNode<'a> {
},
/// This template node is just a piece of static text
Text(&'a str),
Text {
/// The actual text
text: &'a str,
},
/// This template node is unknown, and needs to be created at runtime.
Dynamic(usize),
Dynamic {
/// The index of the dynamic node in the VNode's dynamic_nodes list
id: usize,
},
/// This template node is known to be some text, but needs to be created at runtime
///
/// This is separate from the pure Dynamic variant for various optimizations
DynamicText(usize),
DynamicText {
/// The index of the dynamic node in the VNode's dynamic_nodes list
id: usize,
},
}
/// A node created at runtime
@@ -244,7 +261,11 @@ pub struct VText<'a> {
/// An attribute of the TemplateNode, created at compile time
#[derive(Debug, PartialEq, Hash, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type")
)]
pub enum TemplateAttribute<'a> {
/// This attribute is entirely known at compile time, enabling
Static {
@@ -265,7 +286,10 @@ pub enum TemplateAttribute<'a> {
/// The attribute in this position is actually determined dynamically at runtime
///
/// This is the index into the dynamic_attributes field on the container VNode
Dynamic(usize),
Dynamic {
/// The index
id: usize,
},
}
/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`
@@ -416,6 +440,8 @@ impl<'a> IntoDynNode<'a> for VNode<'a> {
DynamicNode::Fragment(_cx.bump().alloc([self]))
}
}
// An element that's an error is currently lost into the ether
impl<'a> IntoDynNode<'a> for Element<'a> {
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
match self {
+8 -7
View File
@@ -1,5 +1,8 @@
use futures_util::FutureExt;
use std::task::{Context, Poll};
use std::{
rc::Rc,
task::{Context, Poll},
};
use crate::{
innerlude::{Mutation, Mutations, SuspenseContext},
@@ -31,12 +34,10 @@ impl VirtualDom {
}
}
pub(crate) fn acquire_suspense_boundary<'a>(&self, id: ScopeId) -> &'a SuspenseContext {
let ct = self.scopes[id.0]
.consume_context::<SuspenseContext>()
.unwrap();
unsafe { &*(ct as *const SuspenseContext) }
pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc<SuspenseContext> {
self.scopes[id.0]
.consume_context::<Rc<SuspenseContext>>()
.unwrap()
}
pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) {
+6 -38
View File
@@ -7,7 +7,6 @@ use crate::{
scheduler::RcWake,
scopes::{ScopeId, ScopeState},
virtual_dom::VirtualDom,
AttributeValue, DynamicNode, VNode,
};
use bumpalo::Bump;
use futures_util::FutureExt;
@@ -19,7 +18,11 @@ use std::{
};
impl VirtualDom {
pub(super) fn new_scope(&mut self, props: Box<dyn AnyProps<'static>>) -> &ScopeState {
pub(super) fn new_scope(
&mut self,
props: Box<dyn AnyProps<'static>>,
name: &'static str,
) -> &ScopeState {
let parent = self.acquire_current_scope_raw();
let entry = self.scopes.vacant_entry();
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
@@ -30,6 +33,7 @@ impl VirtualDom {
id,
height,
props: Some(props),
name,
placeholder: Default::default(),
node_arena_1: BumpFrame::new(0),
node_arena_2: BumpFrame::new(0),
@@ -50,42 +54,6 @@ impl VirtualDom {
.and_then(|id| self.scopes.get_mut(id.0).map(|f| f.as_mut() as *mut _))
}
fn ensure_drop_safety(&self, scope: ScopeId) {
let scope = &self.scopes[scope.0];
let node = unsafe { scope.previous_frame().try_load_node() };
// And now we want to make sure the previous frame has dropped anything that borrows self
if let Some(RenderReturn::Sync(Ok(node))) = node {
self.ensure_drop_safety_inner(node);
}
}
fn ensure_drop_safety_inner(&self, node: &VNode) {
for attr in node.dynamic_attrs {
if let AttributeValue::Listener(l) = &attr.value {
l.borrow_mut().take();
}
}
for child in node.dynamic_nodes {
match child {
DynamicNode::Component(c) => {
// Only descend if the props are borrowed
if !c.static_props {
self.ensure_drop_safety(c.scope.get().unwrap());
c.props.set(None);
}
}
DynamicNode::Fragment(f) => {
for node in *f {
self.ensure_drop_safety_inner(node);
}
}
_ => {}
}
}
}
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
// Cycle to the next frame and then reset it
// This breaks any latent references, invalidating every pointer referencing into it.
+22 -22
View File
@@ -69,6 +69,7 @@ pub struct ScopeId(pub usize);
/// This struct exists to provide a common interface for all scopes without relying on generics.
pub struct ScopeState {
pub(crate) render_cnt: Cell<usize>,
pub(crate) name: &'static str,
pub(crate) node_arena_1: BumpFrame,
pub(crate) node_arena_2: BumpFrame,
@@ -116,6 +117,11 @@ impl<'src> ScopeState {
}
}
/// Get the name of this component
pub fn name(&self) -> &str {
self.name
}
/// Get the current render since the inception of this component
///
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
@@ -251,17 +257,18 @@ impl<'src> ScopeState {
}
/// Return any context of type T if it exists on this scope
pub fn has_context<T: 'static>(&self) -> Option<&T> {
let contextex = self.shared_contexts.borrow();
let val = contextex.get(&TypeId::of::<T>())?;
let as_concrete = val.downcast_ref::<T>()? as *const T;
Some(unsafe { &*as_concrete })
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
self.shared_contexts
.borrow()
.get(&TypeId::of::<T>())?
.downcast_ref::<T>()
.cloned()
}
/// Try to retrieve a shared state with type `T` from any parent scope.
///
/// To release the borrow, use `cloned` if the context is clone.
pub fn consume_context<T: 'static>(&self) -> Option<&T> {
/// Clones the state if it exists.
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
if let Some(this_ctx) = self.has_context() {
return Some(this_ctx);
}
@@ -271,8 +278,7 @@ impl<'src> ScopeState {
// safety: all parent pointers are valid thanks to the bump arena
let parent = unsafe { &*parent_ptr };
if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
let as_concrete = shared.downcast_ref::<T>()? as *const T;
return Some(unsafe { &*as_concrete });
return shared.downcast_ref::<T>().cloned();
}
search_parent = parent.parent;
}
@@ -284,7 +290,7 @@ impl<'src> ScopeState {
///
/// This is a "fundamental" operation and should only be called during initialization of a hook.
///
/// For a hook that provides the same functionality, use `use_provide_context` and `use_consume_context` instead.
/// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead.
///
/// If a state is provided that already exists, the new value will not be inserted. Instead, this method will
/// return the existing value. This behavior is chosen so shared values do not need to be `Clone`. This particular
@@ -305,20 +311,14 @@ impl<'src> ScopeState {
/// render!(div { "hello {state.0}" })
/// }
/// ```
pub fn provide_context<T: 'static>(&self, value: T) -> &T {
let mut contexts = self.shared_contexts.borrow_mut();
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
let value2 = value.clone();
let any = match contexts.get(&TypeId::of::<T>()) {
Some(item) => item.downcast_ref::<T>().unwrap() as *const T,
None => {
let boxed = Box::new(value);
let boxed_ptr = boxed.as_ref() as *const T;
contexts.insert(TypeId::of::<T>(), boxed);
boxed_ptr
}
};
self.shared_contexts
.borrow_mut()
.insert(TypeId::of::<T>(), Box::new(value));
unsafe { &*any }
value2
}
/// Pushes the future onto the poll queue to be polled after the component renders.
+9 -10
View File
@@ -240,20 +240,19 @@ impl VirtualDom {
mutations: Mutations::default(),
};
let root = dom.new_scope(Box::new(VProps::new(
root,
|_, _| unreachable!(),
root_props,
)));
let root = dom.new_scope(
Box::new(VProps::new(root, |_, _| unreachable!(), root_props)),
"app",
);
// 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(SuspenseContext::new(ScopeId(0)));
root.provide_context(Rc::new(SuspenseContext::new(ScopeId(0))));
// Unlike react, we provide a default error boundary that just renders the error as a string
root.provide_context(ErrorBoundary::new(ScopeId(0)));
root.provide_context(Rc::new(ErrorBoundary::new(ScopeId(0))));
// the root element is always given element ID 0 since it's the container for the entire tree
dom.elements.insert(ElementRef::null());
@@ -297,7 +296,7 @@ impl VirtualDom {
/// currently suspended.
pub fn is_scope_suspended(&self, id: ScopeId) -> bool {
!self.scopes[id.0]
.consume_context::<SuspenseContext>()
.consume_context::<Rc<SuspenseContext>>()
.unwrap()
.waiting_on
.borrow()
@@ -528,7 +527,7 @@ impl VirtualDom {
// first, unload any complete suspense trees
for finished_fiber in self.finished_fibers.drain(..) {
let scope = &mut self.scopes[finished_fiber.0];
let context = scope.has_context::<SuspenseContext>().unwrap();
let context = scope.has_context::<Rc<SuspenseContext>>().unwrap();
self.mutations
.templates
@@ -566,7 +565,7 @@ impl VirtualDom {
// No placeholder necessary since this is a diff
if !self.collected_leaves.is_empty() {
let mut boundary = self.scopes[dirty.id.0]
.consume_context::<SuspenseContext>()
.consume_context::<Rc<SuspenseContext>>()
.unwrap();
let boundary_mut = boundary.borrow_mut();