mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-09-21 14:52:02 +00:00
wip: move from recursive to iterative
This commit is contained in:
parent
f41cff571f
commit
9652ccdcf1
4 changed files with 2173 additions and 29 deletions
|
@ -79,17 +79,44 @@ use std::{
|
|||
};
|
||||
use DomEdit::*;
|
||||
|
||||
/// Our DiffMachine is an iterative tree differ.
|
||||
///
|
||||
/// It uses techniques of a register-based Turing Machines to allow pausing and restarting of the diff algorithm. This
|
||||
/// was origially implemented using recursive techniques, but Rust lacks the abilty to call async functions recursively,
|
||||
/// meaning we could not "pause" the diffing algorithm.
|
||||
///
|
||||
/// Instead, we use a traditional stack machine approach to diff and create new nodes.
|
||||
pub struct DiffMachine<'bump> {
|
||||
vdom: &'bump SharedResources,
|
||||
|
||||
pub mutations: Mutations<'bump>,
|
||||
|
||||
pub nodes_created_stack: SmallVec<[usize; 10]>,
|
||||
|
||||
pub node_stack: SmallVec<[DiffInstruction<'bump>; 10]>,
|
||||
|
||||
pub scope_stack: SmallVec<[ScopeId; 5]>,
|
||||
|
||||
pub diffed: FxHashSet<ScopeId>,
|
||||
|
||||
pub seen_scopes: FxHashSet<ScopeId>,
|
||||
}
|
||||
pub enum DiffInstruction<'a> {
|
||||
DiffNode {
|
||||
old: &'a VNode<'a>,
|
||||
new: &'a VNode<'a>,
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
Append {},
|
||||
Replace {
|
||||
with: usize,
|
||||
},
|
||||
|
||||
Create {
|
||||
node: &'a VNode<'a>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'bump> DiffMachine<'bump> {
|
||||
pub(crate) fn new(
|
||||
|
@ -98,6 +125,8 @@ impl<'bump> DiffMachine<'bump> {
|
|||
shared: &'bump SharedResources,
|
||||
) -> Self {
|
||||
Self {
|
||||
node_stack: smallvec![],
|
||||
nodes_created_stack: smallvec![],
|
||||
mutations: edits,
|
||||
scope_stack: smallvec![cur_scope],
|
||||
vdom: shared,
|
||||
|
@ -111,19 +140,15 @@ impl<'bump> DiffMachine<'bump> {
|
|||
///
|
||||
/// This will PANIC if any component elements are passed in.
|
||||
pub fn new_headless(shared: &'bump SharedResources) -> Self {
|
||||
Self {
|
||||
mutations: Mutations::new(),
|
||||
scope_stack: smallvec![ScopeId(0)],
|
||||
vdom: shared,
|
||||
diffed: FxHashSet::default(),
|
||||
seen_scopes: FxHashSet::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// make incremental progress on the current task
|
||||
pub fn work(&mut self, is_ready: impl FnMut() -> bool) -> Result<FiberResult> {
|
||||
todo!()
|
||||
// Ok(FiberResult::D)
|
||||
// Self {
|
||||
// node_stack: smallvec![],
|
||||
// mutations: Mutations::new(),
|
||||
// scope_stack: smallvec![ScopeId(0)],
|
||||
// vdom: shared,
|
||||
// diffed: FxHashSet::default(),
|
||||
// seen_scopes: FxHashSet::default(),
|
||||
// }
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -134,17 +159,153 @@ impl<'bump> DiffMachine<'bump> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// Diff the `old` node with the `new` node. Emits instructions to modify a
|
||||
// physical DOM node that reflects `old` into something that reflects `new`.
|
||||
//
|
||||
// the real stack should be what it is coming in and out of this function (ideally empty)
|
||||
//
|
||||
// each function call assumes the stack is fresh (empty).
|
||||
pub async fn diff_node(
|
||||
&mut self,
|
||||
old_node: &'bump VNode<'bump>,
|
||||
new_node: &'bump VNode<'bump>,
|
||||
) {
|
||||
/// Progress the diffing for this "fiber"
|
||||
///
|
||||
/// This method implements a depth-first iterative tree traversal.
|
||||
///
|
||||
/// We do depth-first to maintain high cache locality (nodes were originally generated recursively) and because we
|
||||
/// only need a stack (not a queue) of lists
|
||||
///
|
||||
///
|
||||
///
|
||||
pub async fn work(&mut self) -> Result<()> {
|
||||
// todo: don't move the reused instructions around
|
||||
while let Some(mut make) = self.node_stack.pop() {
|
||||
match &mut make {
|
||||
DiffInstruction::DiffNode { old, new, progress } => {
|
||||
//
|
||||
}
|
||||
|
||||
DiffInstruction::Append {} => {
|
||||
let many = self.nodes_created_stack.pop().unwrap();
|
||||
self.edit_append_children(many as u32);
|
||||
}
|
||||
|
||||
DiffInstruction::Replace { with } => {
|
||||
let many = self.nodes_created_stack.pop().unwrap();
|
||||
self.edit_replace_with(*with as u32, many as u32);
|
||||
}
|
||||
|
||||
DiffInstruction::Create { node, .. } => {
|
||||
match &node.kind {
|
||||
VNodeKind::Text(text) => {
|
||||
let real_id = self.vdom.reserve_node();
|
||||
self.edit_create_text_node(text.text, real_id);
|
||||
text.dom_id.set(Some(real_id));
|
||||
*self.nodes_created_stack.last_mut().unwrap() += 1;
|
||||
}
|
||||
VNodeKind::Suspended(suspended) => {
|
||||
let real_id = self.vdom.reserve_node();
|
||||
self.edit_create_placeholder(real_id);
|
||||
suspended.node.set(Some(real_id));
|
||||
*self.nodes_created_stack.last_mut().unwrap() += 1;
|
||||
}
|
||||
VNodeKind::Anchor(anchor) => {
|
||||
let real_id = self.vdom.reserve_node();
|
||||
self.edit_create_placeholder(real_id);
|
||||
anchor.dom_id.set(Some(real_id));
|
||||
*self.nodes_created_stack.last_mut().unwrap() += 1;
|
||||
}
|
||||
|
||||
VNodeKind::Element(el) => {
|
||||
let VElement {
|
||||
tag_name,
|
||||
listeners,
|
||||
attributes,
|
||||
children,
|
||||
namespace,
|
||||
static_attrs: _,
|
||||
static_children: _,
|
||||
static_listeners: _,
|
||||
dom_id,
|
||||
} = el;
|
||||
|
||||
let real_id = self.vdom.reserve_node();
|
||||
self.edit_create_element(tag_name, *namespace, real_id);
|
||||
dom_id.set(Some(real_id));
|
||||
|
||||
let cur_scope = self.current_scope().unwrap();
|
||||
|
||||
listeners.iter().for_each(|listener| {
|
||||
self.fix_listener(listener);
|
||||
listener.mounted_node.set(Some(real_id));
|
||||
self.edit_new_event_listener(listener, cur_scope.clone());
|
||||
|
||||
// if the node has an event listener, then it must be visited ?
|
||||
});
|
||||
|
||||
for attr in *attributes {
|
||||
self.edit_set_attribute(attr);
|
||||
}
|
||||
|
||||
// TODO: the append child edit
|
||||
|
||||
*self.nodes_created_stack.last_mut().unwrap() += 1;
|
||||
|
||||
// push every child onto the stack
|
||||
self.nodes_created_stack.push(0);
|
||||
for child in *children {
|
||||
self.node_stack
|
||||
.push(DiffInstruction::Create { node: child })
|
||||
}
|
||||
}
|
||||
|
||||
VNodeKind::Fragment(frag) => {
|
||||
for node in frag.children {
|
||||
self.node_stack.push(DiffInstruction::Create { node })
|
||||
}
|
||||
}
|
||||
|
||||
VNodeKind::Component(_) => {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create the new node, pushing instructions on our instruction stack to create any further children
|
||||
///
|
||||
///
|
||||
pub fn create_iterative(&mut self, node: &'bump VNode<'bump>) {
|
||||
match &node.kind {
|
||||
// singles
|
||||
// update the parent
|
||||
VNodeKind::Text(text) => {
|
||||
let real_id = self.vdom.reserve_node();
|
||||
self.edit_create_text_node(text.text, real_id);
|
||||
text.dom_id.set(Some(real_id));
|
||||
*self.nodes_created_stack.last_mut().unwrap() += 1;
|
||||
}
|
||||
VNodeKind::Suspended(suspended) => {
|
||||
let real_id = self.vdom.reserve_node();
|
||||
self.edit_create_placeholder(real_id);
|
||||
suspended.node.set(Some(real_id));
|
||||
*self.nodes_created_stack.last_mut().unwrap() += 1;
|
||||
}
|
||||
VNodeKind::Anchor(anchor) => {
|
||||
let real_id = self.vdom.reserve_node();
|
||||
self.edit_create_placeholder(real_id);
|
||||
anchor.dom_id.set(Some(real_id));
|
||||
*self.nodes_created_stack.last_mut().unwrap() += 1;
|
||||
}
|
||||
|
||||
VNodeKind::Element(el) => {
|
||||
//
|
||||
}
|
||||
VNodeKind::Fragment(frag) => {
|
||||
//
|
||||
}
|
||||
VNodeKind::Component(comp) => {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diff_iterative(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) {
|
||||
match (&old_node.kind, &new_node.kind) {
|
||||
// Handle the "sane" cases first.
|
||||
// The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment".
|
||||
|
@ -275,8 +436,7 @@ impl<'bump> DiffMachine<'bump> {
|
|||
false => {
|
||||
// the props are different...
|
||||
scope.run_scope().unwrap();
|
||||
self.diff_node(scope.frames.wip_head(), scope.frames.fin_head())
|
||||
.await;
|
||||
self.diff_node(scope.frames.wip_head(), scope.frames.fin_head());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -308,7 +468,222 @@ impl<'bump> DiffMachine<'bump> {
|
|||
// 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[0], &new.children[0]).await;
|
||||
self.diff_node(&old.children[0], &new.children[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
self.diff_children(old.children, new.children);
|
||||
}
|
||||
|
||||
(VNodeKind::Anchor(old), VNodeKind::Anchor(new)) => {
|
||||
new.dom_id.set(old.dom_id.get());
|
||||
}
|
||||
|
||||
// The strategy here is to pick the first possible node from the previous set and use that as our replace with root
|
||||
//
|
||||
// We also walk the "real node" list to make sure all latent roots are claened up
|
||||
// This covers the case any time a fragment or component shows up with pretty much anything else
|
||||
//
|
||||
// This likely isn't the fastest way to go about replacing one node with a virtual node, but the "insane" cases
|
||||
// are pretty rare. IE replacing a list (component or fragment) with a single node.
|
||||
(
|
||||
VNodeKind::Component(_)
|
||||
| VNodeKind::Fragment(_)
|
||||
| VNodeKind::Text(_)
|
||||
| VNodeKind::Element(_)
|
||||
| VNodeKind::Anchor(_),
|
||||
VNodeKind::Component(_)
|
||||
| VNodeKind::Fragment(_)
|
||||
| VNodeKind::Text(_)
|
||||
| VNodeKind::Element(_)
|
||||
| VNodeKind::Anchor(_),
|
||||
) => {
|
||||
self.replace_and_create_many_with_many([old_node], [new_node]);
|
||||
}
|
||||
|
||||
// TODO
|
||||
(VNodeKind::Suspended(old), new) => {
|
||||
//
|
||||
self.replace_and_create_many_with_many([old_node], [new_node]);
|
||||
}
|
||||
// a node that was once real is now suspended
|
||||
(old, VNodeKind::Suspended(_)) => {
|
||||
//
|
||||
self.replace_and_create_many_with_many([old_node], [new_node]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Diff the `old` node with the `new` node. Emits instructions to modify a
|
||||
// physical DOM node that reflects `old` into something that reflects `new`.
|
||||
//
|
||||
// the real stack should be what it is coming in and out of this function (ideally empty)
|
||||
//
|
||||
// each function call assumes the stack is fresh (empty).
|
||||
pub fn diff_node(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) {
|
||||
match (&old_node.kind, &new_node.kind) {
|
||||
// Handle the "sane" cases first.
|
||||
// The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment".
|
||||
// So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable.
|
||||
(VNodeKind::Text(old), VNodeKind::Text(new)) => {
|
||||
let root = old_node.direct_id();
|
||||
|
||||
if old.text != new.text {
|
||||
self.edit_push_root(root);
|
||||
self.edit_set_text(new.text);
|
||||
self.edit_pop();
|
||||
}
|
||||
|
||||
new.dom_id.set(Some(root));
|
||||
}
|
||||
|
||||
(VNodeKind::Element(old), VNodeKind::Element(new)) => {
|
||||
let root = old_node.direct_id();
|
||||
|
||||
// If the element type is completely different, the element needs to be re-rendered completely
|
||||
// This is an optimization React makes due to how users structure their code
|
||||
//
|
||||
// This case is rather rare (typically only in non-keyed lists)
|
||||
if new.tag_name != old.tag_name || new.namespace != old.namespace {
|
||||
self.replace_node_with_node(root, old_node, new_node);
|
||||
return;
|
||||
}
|
||||
|
||||
new.dom_id.set(Some(root));
|
||||
|
||||
// Don't push the root if we don't have to
|
||||
let mut has_comitted = false;
|
||||
let mut please_commit = |edits: &mut Vec<DomEdit>| {
|
||||
if !has_comitted {
|
||||
has_comitted = true;
|
||||
edits.push(PushRoot { id: root.as_u64() });
|
||||
}
|
||||
};
|
||||
|
||||
// Diff Attributes
|
||||
//
|
||||
// It's extraordinarily rare to have the number/order of attributes change
|
||||
// In these cases, we just completely erase the old set and make a new set
|
||||
//
|
||||
// TODO: take a more efficient path than this
|
||||
if old.attributes.len() == new.attributes.len() {
|
||||
for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) {
|
||||
if old_attr.value != new_attr.value {
|
||||
please_commit(&mut self.mutations.edits);
|
||||
self.edit_set_attribute(new_attr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: provide some sort of report on how "good" the diffing was
|
||||
please_commit(&mut self.mutations.edits);
|
||||
for attribute in old.attributes {
|
||||
self.edit_remove_attribute(attribute);
|
||||
}
|
||||
for attribute in new.attributes {
|
||||
self.edit_set_attribute(attribute)
|
||||
}
|
||||
}
|
||||
|
||||
// Diff listeners
|
||||
//
|
||||
// It's extraordinarily rare to have the number/order of listeners change
|
||||
// In the cases where the listeners change, we completely wipe the data attributes and add new ones
|
||||
//
|
||||
// We also need to make sure that all listeners are properly attached to the parent scope (fix_listener)
|
||||
//
|
||||
// TODO: take a more efficient path than this
|
||||
let cur_scope: ScopeId = self.scope_stack.last().unwrap().clone();
|
||||
if old.listeners.len() == new.listeners.len() {
|
||||
for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) {
|
||||
if old_l.event != new_l.event {
|
||||
please_commit(&mut self.mutations.edits);
|
||||
self.edit_remove_event_listener(old_l.event);
|
||||
self.edit_new_event_listener(new_l, cur_scope);
|
||||
}
|
||||
new_l.mounted_node.set(old_l.mounted_node.get());
|
||||
self.fix_listener(new_l);
|
||||
}
|
||||
} else {
|
||||
please_commit(&mut self.mutations.edits);
|
||||
for listener in old.listeners {
|
||||
self.edit_remove_event_listener(listener.event);
|
||||
}
|
||||
for listener in new.listeners {
|
||||
listener.mounted_node.set(Some(root));
|
||||
self.edit_new_event_listener(listener, cur_scope);
|
||||
|
||||
// Make sure the listener gets attached to the scope list
|
||||
self.fix_listener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
if has_comitted {
|
||||
self.edit_pop();
|
||||
}
|
||||
|
||||
self.diff_children(old.children, new.children);
|
||||
}
|
||||
|
||||
(VNodeKind::Component(old), VNodeKind::Component(new)) => {
|
||||
let scope_addr = old.ass_scope.get().unwrap();
|
||||
|
||||
// Make sure we're dealing with the same component (by function pointer)
|
||||
if old.user_fc == new.user_fc {
|
||||
//
|
||||
self.scope_stack.push(scope_addr);
|
||||
|
||||
// Make sure the new component vnode is referencing the right scope id
|
||||
new.ass_scope.set(Some(scope_addr));
|
||||
|
||||
// make sure the component's caller function is up to date
|
||||
let scope = self.get_scope_mut(&scope_addr).unwrap();
|
||||
|
||||
scope
|
||||
.update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children));
|
||||
|
||||
// React doesn't automatically memoize, but we do.
|
||||
let compare = old.comparator.unwrap();
|
||||
|
||||
match compare(new) {
|
||||
true => {
|
||||
// the props are the same...
|
||||
}
|
||||
false => {
|
||||
// the props are different...
|
||||
scope.run_scope().unwrap();
|
||||
self.diff_node(scope.frames.wip_head(), scope.frames.fin_head());
|
||||
}
|
||||
}
|
||||
|
||||
self.scope_stack.pop();
|
||||
|
||||
self.seen_scopes.insert(scope_addr);
|
||||
} else {
|
||||
let mut old_iter = RealChildIterator::new(old_node, &self.vdom);
|
||||
let first = old_iter
|
||||
.next()
|
||||
.expect("Components should generate a placeholder root");
|
||||
|
||||
// remove any leftovers
|
||||
for to_remove in old_iter {
|
||||
self.edit_push_root(to_remove.direct_id());
|
||||
self.edit_remove();
|
||||
}
|
||||
|
||||
// seems like we could combine this into a single instruction....
|
||||
self.replace_node_with_node(first.direct_id(), old_node, new_node);
|
||||
|
||||
// Wipe the old one and plant the new one
|
||||
let old_scope = old.ass_scope.get().unwrap();
|
||||
self.destroy_scopes(old_scope);
|
||||
}
|
||||
}
|
||||
|
||||
(VNodeKind::Fragment(old), VNodeKind::Fragment(new)) => {
|
||||
// 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[0], &new.children[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1116,14 +1491,14 @@ impl<'bump> DiffMachine<'bump> {
|
|||
|
||||
// diff the rest
|
||||
for (new_child, old_child) in new.iter().zip(old.iter()) {
|
||||
self.diff_node(old_child, new_child).await
|
||||
self.diff_node(old_child, new_child)
|
||||
}
|
||||
}
|
||||
|
||||
// old.len == new.len -> no nodes added/removed, but perhaps changed
|
||||
Ordering::Equal => {
|
||||
for (new_child, old_child) in new.iter().zip(old.iter()) {
|
||||
self.diff_node(old_child, new_child).await;
|
||||
self.diff_node(old_child, new_child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1659,3 +2034,16 @@ fn compare_strs(a: &str, b: &str) -> bool {
|
|||
true
|
||||
}
|
||||
}
|
||||
|
||||
struct DfsIterator<'a> {
|
||||
idx: usize,
|
||||
node: Option<(&'a VNode<'a>, &'a VNode<'a>)>,
|
||||
nodes: Option<(&'a [VNode<'a>], &'a [VNode<'a>])>,
|
||||
}
|
||||
impl<'a> Iterator for DfsIterator<'a> {
|
||||
type Item = (&'a VNode<'a>, &'a VNode<'a>);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
1708
packages/core/src/diff.rs.old
Normal file
1708
packages/core/src/diff.rs.old
Normal file
File diff suppressed because it is too large
Load diff
|
@ -285,7 +285,8 @@ impl Scheduler {
|
|||
let mut is_ready = || -> bool { (&mut deadline).now_or_never().is_some() };
|
||||
|
||||
// TODO: remove this unwrap - proprogate errors out
|
||||
self.get_current_fiber().work(is_ready).unwrap()
|
||||
// self.get_current_fiber().work(is_ready).unwrap()
|
||||
todo!()
|
||||
}
|
||||
|
||||
// waits for a trigger, canceling early if the deadline is reached
|
||||
|
|
47
packages/core/tests/iterative.rs
Normal file
47
packages/core/tests/iterative.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
//! tests to prove that the iterative implementation works
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use dioxus::{
|
||||
arena::SharedResources,
|
||||
diff::{CreateMeta, DiffMachine},
|
||||
prelude::*,
|
||||
scheduler::Mutations,
|
||||
DomEdit,
|
||||
};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
#[test]
|
||||
fn test_original_diff() {
|
||||
static App: FC<()> = |cx| {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
div {
|
||||
"Hello, world!"
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let mutations = dom.rebuild().unwrap();
|
||||
dbg!(mutations);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_iterative_diff() {
|
||||
static App: FC<()> = |cx| {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
div {
|
||||
"Hello, world!"
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let shared = SharedResources::new();
|
||||
|
||||
let mut machine = DiffMachine::new_headless(&shared);
|
||||
let a = machine.work().await.unwrap();
|
||||
}
|
Loading…
Reference in a new issue