wip: ....sigh..... so the diffing algorithm is robust
but it's still not finished. We need to re-enable the fancier keyed diffing versions some point (soon!).
This commit is contained in:
parent
ff0a3d1c83
commit
68ed1c04e7
|
@ -62,3 +62,4 @@ attr
|
|||
derefed
|
||||
Tokio
|
||||
asynchronicity
|
||||
constified
|
||||
|
|
22
README.md
22
README.md
|
@ -36,6 +36,28 @@ If you know React, then you already know Dioxus.
|
|||
- Powerful and simple integrated state management
|
||||
- Rust! (enums, static types, modules, efficiency)
|
||||
|
||||
### Unique features:
|
||||
- Incredible inline documentation - supports hover and guides for all HTML elements, listeners, and events.
|
||||
- Templates are "constified" at compile time - nodes that don't change won't be unnecessarily diffed.
|
||||
- Rust's smart pointers make use_state extremely easy and fun to work with.
|
||||
- Starting a new app takes zero templates or special tools - get a new app running in just seconds.
|
||||
- Desktop apps running natively (no Electron!) in less than 10 lines of code.
|
||||
- Multiple flavors (raw, html, rsx) of templating to pick-and-choose for the type of component.
|
||||
- Extensible DSL through traits and impls plus support for WebComponents.
|
||||
- Simple-to-implement trait for new reconcilers.
|
||||
- Fast retained-mode server-side-renderer that works with buffered writers, files, and strings.
|
||||
- Compile-time correct inline CSS as well as global styling.
|
||||
- Integrated "signal" system allows for near-instant updates by skipping the diff algorithm entirely.
|
||||
- Built-in cooperative scheduler (IE React's fiber mechanism).
|
||||
- Built-in asynchronous scheduler for coroutines and tasks within components.
|
||||
- Built-in suspense scheduler for Rust's futures.
|
||||
- Custom bump-allocator backing for all components. Nearly 0 allocations for steady-state components.
|
||||
- Extremely fast diffing algorithm for the most complex of apps.
|
||||
- Runs natively on mobile and on desktop with no need for a 3rd party JS engine.
|
||||
- Drastically fewer runtime errors and crashes with first-class error handling.
|
||||
- Extremely powerful iterator and optional chaining integration for fast and robust apps.
|
||||
- The most ergonomic and powerful state management of any Rust UI toolkit.
|
||||
|
||||
## Get Started with...
|
||||
|
||||
<table style="width:100%" align="center">
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
use std::cell::Cell;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::{
|
||||
nodes::{NodeKey, VElement, VText},
|
||||
RealDomNode,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
@ -13,8 +19,18 @@ p {color: red;}
|
|||
|
||||
const Example: FC<()> = |cx| {
|
||||
cx.render(rsx! {
|
||||
Child { }
|
||||
Child { }
|
||||
Fragment {
|
||||
Fragment {
|
||||
Fragment {
|
||||
"h1"
|
||||
}
|
||||
"h2"
|
||||
}
|
||||
"h3"
|
||||
}
|
||||
"h4"
|
||||
div { "h5" }
|
||||
Child {}
|
||||
})
|
||||
};
|
||||
|
||||
|
@ -26,3 +42,22 @@ const Child: FC<()> = |cx| {
|
|||
h1 {"4" }
|
||||
))
|
||||
};
|
||||
|
||||
// this is a bad case that hurts our subtree memoization :(
|
||||
const AbTest: FC<()> = |cx| {
|
||||
if 1 == 2 {
|
||||
cx.render(rsx!(
|
||||
h1 {"1"}
|
||||
h1 {"2"}
|
||||
h1 {"3"}
|
||||
h1 {"4"}
|
||||
))
|
||||
} else {
|
||||
cx.render(rsx!(
|
||||
h1 {"1"}
|
||||
h1 {"2"}
|
||||
h2 {"basd"}
|
||||
h1 {"4"}
|
||||
))
|
||||
}
|
||||
};
|
||||
|
|
|
@ -45,3 +45,8 @@ div {
|
|||
h4 { "{sig3}" }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Subtree memoization
|
||||
|
||||
The rsx! macro needs to be *really* smart. If it detects that no dynamics are pumped into the macro, then it opts to use the "const" flavors of the element build functions we know and love. This has to be done at build time rather than runtime since components may return basically anything. Using the const flavor enables is_static which encourages Dioxus to do a ptr compare instead of a value compare to short circuit through the diffing. Due to const folding in Rust, entire subtrees can be ruled out at compile time.
|
||||
|
|
|
@ -17,6 +17,7 @@ pub struct Element {
|
|||
attributes: Vec<ElementAttr>,
|
||||
listeners: Vec<ElementAttr>,
|
||||
children: Vec<Node>,
|
||||
is_static: bool,
|
||||
}
|
||||
|
||||
impl ToTokens for Element {
|
||||
|
@ -100,6 +101,7 @@ impl Parse for Element {
|
|||
attributes,
|
||||
children,
|
||||
listeners,
|
||||
is_static: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ use std::any::Any;
|
|||
/// target-specific Node type as well as easily serializing the edits to be sent over a network or IPC connection.
|
||||
pub trait RealDom<'a> {
|
||||
// Navigation
|
||||
fn push_root(&mut self, root: RealDomNode);
|
||||
fn push(&mut self, root: RealDomNode);
|
||||
fn pop(&mut self);
|
||||
|
||||
// Add Nodes to the dom
|
||||
|
@ -168,7 +168,7 @@ where
|
|||
}
|
||||
|
||||
if old.text != new.text {
|
||||
self.dom.push_root(old.dom_id.get());
|
||||
self.dom.push(old.dom_id.get());
|
||||
log::debug!("Text has changed {}, {}", old.text, new.text);
|
||||
self.dom.set_text(new.text);
|
||||
self.dom.pop();
|
||||
|
@ -183,7 +183,7 @@ where
|
|||
//
|
||||
// In Dioxus, this is less likely to occur unless through a fragment
|
||||
if new.tag_name != old.tag_name || new.namespace != old.namespace {
|
||||
self.dom.push_root(old.dom_id.get());
|
||||
self.dom.push(old.dom_id.get());
|
||||
let meta = self.create(new_node);
|
||||
self.dom.replace_with(meta.added_to_stack);
|
||||
self.dom.pop();
|
||||
|
@ -194,7 +194,7 @@ where
|
|||
new.dom_id.set(oldid);
|
||||
|
||||
// push it just in case
|
||||
self.dom.push_root(oldid);
|
||||
self.dom.push(oldid);
|
||||
self.diff_listeners(old.listeners, new.listeners);
|
||||
self.diff_attr(old.attributes, new.attributes, new.namespace);
|
||||
self.diff_children(old.children, new.children);
|
||||
|
@ -210,65 +210,48 @@ where
|
|||
let scope_id = old.ass_scope.get();
|
||||
new.ass_scope.set(scope_id);
|
||||
|
||||
// new.mounted_root.set(old.mounted_root.get());
|
||||
|
||||
// make sure the component's caller function is up to date
|
||||
let scope = self.components.try_get_mut(scope_id.unwrap()).unwrap();
|
||||
// .with_scope(scope_id.unwrap(), |scope| {
|
||||
|
||||
scope.caller = new.caller.clone();
|
||||
|
||||
// ack - this doesn't work on its own!
|
||||
scope.update_children(new.children);
|
||||
|
||||
// })
|
||||
// .unwrap();
|
||||
|
||||
// React doesn't automatically memoize, but we do.
|
||||
// The cost is low enough to make it worth checking
|
||||
// Rust produces fairly performant comparison methods, sometimes SIMD accelerated
|
||||
// let should_render = match old.comparator {
|
||||
// Some(comparator) => comparator(new),
|
||||
// None => true,
|
||||
// };
|
||||
let should_render = match old.comparator {
|
||||
Some(comparator) => comparator(new),
|
||||
None => true,
|
||||
};
|
||||
|
||||
if should_render {
|
||||
scope.run_scope().unwrap();
|
||||
self.diff_node(scope.old_frame(), scope.next_frame());
|
||||
} else {
|
||||
//
|
||||
}
|
||||
|
||||
// if should_render {
|
||||
// // self.dom.commit_traversal();
|
||||
// let f = self.components.try_get_mut(scope_id.unwrap()).unwrap();
|
||||
// self.components
|
||||
// .with_scope(scope_id.unwrap(), |f| {
|
||||
// log::debug!("running scope during diff {:#?}", scope_id);
|
||||
scope.run_scope().unwrap();
|
||||
self.diff_node(scope.old_frame(), scope.next_frame());
|
||||
// log::debug!("scope completed {:#?}", scope_id);
|
||||
self.seen_nodes.insert(scope_id.unwrap());
|
||||
// })
|
||||
// .unwrap();
|
||||
|
||||
// diff_machine.change_list.load_known_root(root_id);
|
||||
// run the scope
|
||||
//
|
||||
// } else {
|
||||
// log::error!("Memoized componented");
|
||||
// // Component has memoized itself and doesn't need to be re-rendered.
|
||||
// // We still need to make sure the child's props are up-to-date.
|
||||
// // Don't commit traversal
|
||||
// }
|
||||
} else {
|
||||
// It's an entirely different component
|
||||
// this seems to be a fairy common code path that we could
|
||||
let mut old_iter = RealChildIterator::new(old_node, &self.components);
|
||||
let first = old_iter
|
||||
.next()
|
||||
.expect("Components should generate a placeholder root");
|
||||
|
||||
// A new component has shown up! We need to destroy the old node
|
||||
// remove any leftovers
|
||||
for to_remove in old_iter {
|
||||
self.dom.push(to_remove);
|
||||
self.dom.remove();
|
||||
}
|
||||
|
||||
// seems like we could combine this into a single instruction....
|
||||
self.dom.push(first);
|
||||
let meta = self.create(new_node);
|
||||
self.dom.replace_with(meta.added_to_stack);
|
||||
self.dom.pop();
|
||||
|
||||
// Wipe the old one and plant the new one
|
||||
// self.dom.commit_traversal();
|
||||
// self.dom.replace_node_with(old.dom_id, new.dom_id);
|
||||
// self.create(new_node);
|
||||
log::warn!("creating and replacing...");
|
||||
self.create(new_node);
|
||||
|
||||
// self.dom.replace_with();
|
||||
// self.create_and_repalce(new_node, old.mounted_root.get());
|
||||
|
||||
// Now we need to remove the old scope and all of its descendents
|
||||
let old_scope = old.ass_scope.get().unwrap();
|
||||
self.destroy_scopes(old_scope);
|
||||
}
|
||||
|
@ -278,7 +261,7 @@ where
|
|||
// This is the case where options or direct vnodes might be used.
|
||||
// In this case, it's faster to just skip ahead to their diff
|
||||
if old.children.len() == 1 && new.children.len() == 1 {
|
||||
self.diff_node(old.children.get(0).unwrap(), new.children.get(0).unwrap());
|
||||
self.diff_node(&old.children[0], &new.children[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -300,6 +283,7 @@ where
|
|||
) => {
|
||||
// Choose the node to use as the placeholder for replacewith
|
||||
let back_node = match old_node {
|
||||
// We special case these two types to avoid allocating the small-vecs
|
||||
VNode::Element(_) | VNode::Text(_) => old_node
|
||||
.get_mounted_id(&self.components)
|
||||
.expect("Element and text always have a real node"),
|
||||
|
@ -313,7 +297,7 @@ where
|
|||
|
||||
// remove any leftovers
|
||||
for to_remove in old_iter {
|
||||
self.dom.push_root(to_remove);
|
||||
self.dom.push(to_remove);
|
||||
self.dom.remove();
|
||||
}
|
||||
|
||||
|
@ -322,9 +306,11 @@ where
|
|||
};
|
||||
|
||||
// replace the placeholder or first node with the nodes generated from the "new"
|
||||
self.dom.push_root(back_node);
|
||||
self.dom.push(back_node);
|
||||
let meta = self.create(new_node);
|
||||
self.dom.replace_with(meta.added_to_stack);
|
||||
|
||||
// todo use the is_static metadata to update this subtree
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
@ -1317,9 +1303,10 @@ enum KeyedPrefixResult {
|
|||
struct RealChildIterator<'a> {
|
||||
scopes: &'a SharedArena,
|
||||
|
||||
// Heuristcally we should never bleed into 5 completely nested fragments/components
|
||||
// Heuristcally we should never bleed into 4 completely nested fragments/components
|
||||
// Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane
|
||||
stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>,
|
||||
// TODO: use const generics instead of the 4 estimation
|
||||
stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 4]>,
|
||||
}
|
||||
|
||||
impl<'a> RealChildIterator<'a> {
|
||||
|
|
|
@ -18,6 +18,7 @@ pub enum DomEdit<'bump> {
|
|||
PushRoot {
|
||||
root: u64,
|
||||
},
|
||||
PopRoot,
|
||||
AppendChildren {
|
||||
many: u32,
|
||||
},
|
||||
|
|
|
@ -62,7 +62,7 @@ impl RealDomNode {
|
|||
pub fn new(id: u64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
pub fn empty() -> Self {
|
||||
pub const fn empty() -> Self {
|
||||
Self(u64::MIN)
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ impl DebugDom {
|
|||
}
|
||||
}
|
||||
impl<'a> RealDom<'a> for DebugDom {
|
||||
fn push_root(&mut self, root: RealDomNode) {}
|
||||
fn push(&mut self, root: RealDomNode) {}
|
||||
fn pop(&mut self) {}
|
||||
|
||||
fn append_children(&mut self, many: u32) {}
|
||||
|
|
|
@ -71,32 +71,46 @@ impl WebsysDom {
|
|||
}
|
||||
|
||||
impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
|
||||
fn push_root(&mut self, root: RealDomNode) {
|
||||
fn push(&mut self, root: RealDomNode) {
|
||||
log::debug!("Called [push_root] {:?}", root);
|
||||
let key: DefaultKey = KeyData::from_ffi(root.0).into();
|
||||
let domnode = self.nodes.get(key).expect("Failed to pop know root");
|
||||
self.stack.push(domnode.clone());
|
||||
}
|
||||
// drop the node off the stack
|
||||
fn pop(&mut self) {
|
||||
self.stack.pop();
|
||||
}
|
||||
|
||||
fn append_children(&mut self, many: u32) {
|
||||
log::debug!("Called [`append_child`]");
|
||||
let child = self.stack.pop();
|
||||
|
||||
if child.dyn_ref::<web_sys::Text>().is_some() {
|
||||
if self.last_node_was_text {
|
||||
let comment_node = self
|
||||
.document
|
||||
.create_comment("dioxus")
|
||||
.dyn_into::<Node>()
|
||||
.unwrap();
|
||||
self.stack.top().append_child(&comment_node).unwrap();
|
||||
let mut root: Node = self
|
||||
.stack
|
||||
.list
|
||||
.get(self.stack.list.len() - many as usize)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
for _ in 0..many {
|
||||
let child = self.stack.pop();
|
||||
|
||||
if child.dyn_ref::<web_sys::Text>().is_some() {
|
||||
if self.last_node_was_text {
|
||||
let comment_node = self
|
||||
.document
|
||||
.create_comment("dioxus")
|
||||
.dyn_into::<Node>()
|
||||
.unwrap();
|
||||
self.stack.top().append_child(&comment_node).unwrap();
|
||||
}
|
||||
self.last_node_was_text = true;
|
||||
} else {
|
||||
self.last_node_was_text = false;
|
||||
}
|
||||
self.last_node_was_text = true;
|
||||
} else {
|
||||
self.last_node_was_text = false;
|
||||
}
|
||||
|
||||
self.stack.top().append_child(&child).unwrap();
|
||||
root.append_child(&child).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_with(&mut self, many: u32) {
|
||||
|
@ -104,35 +118,31 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
|
|||
let new_node = self.stack.pop();
|
||||
let old_node = self.stack.pop();
|
||||
|
||||
if old_node.has_type::<Element>() {
|
||||
old_node
|
||||
.dyn_ref::<Element>()
|
||||
.unwrap()
|
||||
.replace_with_with_node_1(&new_node)
|
||||
.unwrap();
|
||||
} else if old_node.has_type::<web_sys::CharacterData>() {
|
||||
old_node
|
||||
.dyn_ref::<web_sys::CharacterData>()
|
||||
.unwrap()
|
||||
.replace_with_with_node_1(&new_node)
|
||||
.unwrap();
|
||||
} else if old_node.has_type::<web_sys::DocumentType>() {
|
||||
old_node
|
||||
.dyn_ref::<web_sys::DocumentType>()
|
||||
.unwrap()
|
||||
.replace_with_with_node_1(&new_node)
|
||||
.unwrap();
|
||||
} else {
|
||||
panic!("Cannot replace node: {:?}", old_node);
|
||||
// TODO: use different-sized replace withs
|
||||
if many == 1 {
|
||||
if old_node.has_type::<Element>() {
|
||||
old_node
|
||||
.dyn_ref::<Element>()
|
||||
.unwrap()
|
||||
.replace_with_with_node_1(&new_node)
|
||||
.unwrap();
|
||||
} else if old_node.has_type::<web_sys::CharacterData>() {
|
||||
old_node
|
||||
.dyn_ref::<web_sys::CharacterData>()
|
||||
.unwrap()
|
||||
.replace_with_with_node_1(&new_node)
|
||||
.unwrap();
|
||||
} else if old_node.has_type::<web_sys::DocumentType>() {
|
||||
old_node
|
||||
.dyn_ref::<web_sys::DocumentType>()
|
||||
.unwrap()
|
||||
.replace_with_with_node_1(&new_node)
|
||||
.unwrap();
|
||||
} else {
|
||||
panic!("Cannot replace node: {:?}", old_node);
|
||||
}
|
||||
}
|
||||
|
||||
// // poc to see if this is a valid solution
|
||||
// if let Some(id) = self.current_known {
|
||||
// // update mapping
|
||||
// self.known_roots.insert(id, new_node.clone());
|
||||
// self.current_known = None;
|
||||
// }
|
||||
|
||||
self.stack.push(new_node);
|
||||
}
|
||||
|
||||
|
@ -303,7 +313,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
|
|||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Stack {
|
||||
list: Vec<Node>,
|
||||
pub list: Vec<Node>,
|
||||
}
|
||||
|
||||
impl Stack {
|
||||
|
|
|
@ -31,9 +31,12 @@ impl WebviewDom<'_> {
|
|||
}
|
||||
}
|
||||
impl<'bump> RealDom<'bump> for WebviewDom<'bump> {
|
||||
fn push_root(&mut self, root: RealDomNode) {
|
||||
fn push(&mut self, root: RealDomNode) {
|
||||
self.edits.push(PushRoot { root: root.0 });
|
||||
}
|
||||
fn pop(&mut self) {
|
||||
self.edits.push(PopRoot {});
|
||||
}
|
||||
|
||||
fn append_children(&mut self, many: u32) {
|
||||
self.edits.push(AppendChildren { many });
|
||||
|
|
Loading…
Reference in New Issue