diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index 4d0fbe798..5c73ac785 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -63,3 +63,4 @@ derefed Tokio asynchronicity constified +SegVec diff --git a/packages/core/architecture.md b/packages/core/architecture.md index deafc6be7..3348f246a 100644 --- a/packages/core/architecture.md +++ b/packages/core/architecture.md @@ -1,64 +1,101 @@ -# This module includes all life-cycle related mechanics, including the virtual DOM, scopes, properties, and lifecycles. +# Dioxus Core Architecture: ---- +Main topics covered here: +- Fiber, Concurrency, and Cooperative Scheduling +- Suspense +- Signals +- Patches +- Diffing +- Const/Static structures +- Components/Scope +- Hooks +- VNode Bump Arenas -The VirtualDom is designed as so: +## Components/Scope -VDOM contains: +All components in Dioxus are backed by something called the `Scope`. As a user, you will never directly interact with the `Scope` as most calls are shuttled through `Context`. Scopes manage all the internal state for components including hooks, bump arenas, and (unsafe!) lifetime management. -- An arena of component scopes. - - A scope contains - - lifecycle data - - hook data -- Event queue - - An event +Whenever a new component is created from within a user's component, a new "caller" closure is created that captures the component's properties. In contrast to Yew, we allow components to borrow from their parents, provided their `memo` (essentially canComponentUpdate) method returns false for non-static items. The `Props` macro figures this out automatically, and implements the `Properties` trait with the correct `canComponentUpdate` flag. Implementing this method manually is unsafe! With the `Props` macro you can manually disable memoization, but cannot manually enable memoization for non 'static properties structs without invoking unsafe. For 99% of cases, this is fine. -A VDOM is +During diffing, the "caller" closure is updated if the props are not `static. This is very important! If the props change, the old version will be referencing stale data that exists in an "old" bump frame. However, if we cycled the bump frames twice without updating the closure, then the props will point to invalid data and cause memory safety issues. Therefore, it's an invariant to ensure that non-'static Props are always updated during diffing. -- constructed from anything that implements "component" +## Hooks -A "Component" is anything (normally functions) that can be ran with a context to produce VNodes +Hooks are a form of state that's slightly more finicky than structs but more extensible overall. Hooks cannot be used in conditionals, but are portable enough to run on most targets. -- Must implement properties-builder trait which produces a properties builder +The Dioxus hook model uses a Bump arena where user's data lives. -A Context +Initializing hooks: +- The component is created +- The virtualdom heuristics engine pre-allocates a calculated set of memory for the bump arena +- The component is called through "run_scope" +- Each call to use_hook allocates new data for the hook, stores the raw pointer, and pushes the hook index forward +- Each call to use_hook also stores a function pointer fn() that will be used to clean up the hook +- Once the component is finished running, the hook index is reset and the bump is shrunk to shrunk to fit +- The final size of the bump is then used in the heuristics engine for future components. -- Is a consumable struct - - Made of references to properties - - Holds a reference (lockable) to the underlying scope - - Is partially thread-safe +Running hooks: +- Each time use_hook is called, the internal hook state is fetched as &mut T +- We are guaranteed that our &mut T is not aliasing by re-generating any &mut T dependencies +- The hook counter is incremented -# How to interact with the real dom? -## idea: use only u32 +Dropping hooks: +- When the hook is scheduled for deletion, the "drop" function is run for each hook +- (dropping hooks is basically a drop implementation, but can be customized even for primitives) -pros: -- allows for 4,294,967,295 nodes (enough) -- u32 is relatively small -- doesn't add type noise -- allows virtualdom to stay completely generic +## VNode Bump Arenas -cons: -- cost of querying individual nodes (about 7ns per node query for all sizes w/ nohasher) -- 2-3 ns query cost with slotmap -- old IDs need to be manually freed when subtrees are destroyed - - can be collected as garbage after every render -- loss of ids between renders........................ - - each new render doesn't know which node the old one was connected to unless it is visited - - When are nodes _not_ visited during diffing? - - They are predetermined to be removed (a parent was probed) - - something with keys? - - I think all nodes must be visited between diffs - - -## idea: leak raw nodes and then reclaim them on drop +## Diffing -# Fiber/Concurrency +The entire diffing logic for Dioxus lives in one file (diff.rs). Diffing in Dioxus is hyper-optimized for the types of structures generated by the rsx! and html! macros. -Dioxus is designed to support partial rendering. Partial rendering means that not _every_ component will be rendered on every tick. If some components were diffed. +The diffing engine in Dioxus expects the RealDom -Any given component will only be rendered on a single thread, so data inside of components does not need to be send/sync. -To schedule a render outside of the main component, the `suspense` method is exposed. `Suspense` consumes a future (valid for `bump) lifetime +## Patches + +Dioxus uses patches - not imperative methods - to modify the real dom. This speeds up the diffing operation and makes diffing cancelable which is useful for cooperative scheduling. In general, the RealDom trait exists so renderers can share "Node pointers" across runtime boundaries. + +There are no contractual obligations between the VirtualDOM and RealDOM. When the VirtualDOM finishes its work, it releases a Vec of Edits (patches) which the RealDOM can use to update itself. + + + +## Fiber/Concurrency and Cooperative Scheduling + +When an EventTrigger enters the queue and "progress" is called (an async function), Dioxus will get to work running scopes and diffing nodes. Scopes are run and nodes are diffed together. Dioxus records which scopes get diffed to track the progress of its work. + +While descending through the stack frame, Dioxus will query the RealDom for "time remaining." When the time runs out, Dioxus will escape the stack frame by queuing whatever work it didn't get to, and then bubbling up out of "diff_node". Dioxus will also bubble out of "diff_node" if more important work gets queued while it was descending. + +Once bubbled out of diff_node, Dioxus will request the next idle callback and await for it to become available. The return of this callback is a "Deadline" object which Dioxus queries through the RealDom. + +All of this is orchestrated to keep high priority events moving through the VirtualDOM and scheduling lower-priority work around the RealDOM's animations and periodic tasks. +```js +// returns a "deadline" object +function idle() { + return new Promise(resolve => requestIdleCallback(resolve)); +} +``` + +## Suspense +In React, "suspense" is the ability render nodes outside of the traditional lifecycle. React will wait on a future to complete, and once the data is ready, will render those nodes. React's version of suspense is designed to make working with promises in components easier. + + +In Dioxus, we have similar philosophy, but the use and details of suspense is slightly different. For starters, we don't currently allow using futures in the element structure. Technically, we can allow futures - and we do with "Signals" - but the "suspense" feature itself is meant to be self-contained within a single component. This forces you to handle all the loading states within your component, instead of outside the component, keeping things a bit more containerized. + +Internally, the flow of suspense works like this: + +1. accept the user's future. the future must be owned. +2. wrap that owned future with a new future that returns an EventTrigger +3. submit the future to the VirtualDOM's task queue +4. Poll the task queue in the VirtualDOM's event loop +5. use the EventTrigger from the future to find the use_suspense hook again +6. run that hook's callback with the component's bump arena and result of the future +7. set the hook's "inner value" with the completed valued so futures calls can resolve instantly +8. load the original placeholder node (either a suspended node or an actual node) +9. diff that node with the new node with a low priority on its own fiber +10. return the patches back to the event loop +11. apply the patches to the real dom diff --git a/packages/core/examples/async.rs b/packages/core/examples/async.rs index 9b4509dc9..2734bf3b5 100644 --- a/packages/core/examples/async.rs +++ b/packages/core/examples/async.rs @@ -1,8 +1,5 @@ - - use dioxus_core::prelude::*; - fn main() {} const App: FC<()> = |cx| { @@ -24,6 +21,12 @@ const App: FC<()> = |cx| { }; const Task: FC<()> = |cx| { + let (task, res) = cx.use_task(|| async { true }); + // task.pause(); + // task.restart(); + // task.stop(); + // task.drop(); + // let _s = cx.use_task(|| async { "hello world".to_string() }); diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index fdc35809f..a8b8596a6 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -299,7 +299,10 @@ Any function prefixed with "use" should not be called conditionally. /// Awaits the given task, forcing the component to re-render when the value is ready. /// /// - pub fn use_task(&self, task_initializer: Init) -> &mut Option + pub fn use_task( + &self, + task_initializer: Init, + ) -> (&TaskHandle, &mut Option) where Out: 'static, Fut: Future, @@ -342,7 +345,7 @@ Any function prefixed with "use" should not be called conditionally. if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() { hook.value = Some(val); } - &mut hook.value + (&TaskHandle { _p: PhantomData }, &mut hook.value) }, |_| {}, ) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 560cef5a9..d6dee1150 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -65,50 +65,104 @@ use std::any::Any; /// any modifications that follow. This technique enables the diffing algorithm to avoid directly handling or storing 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> { + fn request_available_node(&mut self) -> RealDomNode; + // node ref + fn raw_node_as_any_mut(&self) -> &mut dyn Any; +} + +pub struct DomEditor<'real, 'bump> { + edits: &'real mut Vec>, +} +use DomEdit::*; +impl<'real, 'bump> DomEditor<'real, 'bump> { // Navigation - fn push(&mut self, root: RealDomNode); - fn pop(&mut self); + pub(crate) fn push(&mut self, root: RealDomNode) { + self.edits.push(PushRoot { root: root.0 }); + } + pub(crate) fn pop(&mut self) { + self.edits.push(PopRoot {}); + } // Add Nodes to the dom // add m nodes from the stack - fn append_children(&mut self, many: u32); + pub(crate) fn append_children(&mut self, many: u32) { + self.edits.push(AppendChildren { many }); + } // replace the n-m node on the stack with the m nodes // ends with the last element of the chain on the top of the stack - fn replace_with(&mut self, many: u32); + pub(crate) fn replace_with(&mut self, many: u32) { + self.edits.push(ReplaceWith { many }); + } // Remove Nodesfrom the dom - fn remove(&mut self); - fn remove_all_children(&mut self); + pub(crate) fn remove(&mut self) { + self.edits.push(Remove); + } + pub(crate) fn remove_all_children(&mut self) { + self.edits.push(RemoveAllChildren); + } // Create - fn create_text_node(&mut self, text: &'a str) -> RealDomNode; - fn create_element(&mut self, tag: &'static str, ns: Option<&'static str>) -> RealDomNode; + pub(crate) fn create_text_node(&mut self, text: &'bump str, id: RealDomNode) { + self.edits.push(CreateTextNode { text, id: id.0 }); + } + pub(crate) fn create_element( + &mut self, + tag: &'static str, + ns: Option<&'static str>, + id: RealDomNode, + ) { + match ns { + Some(ns) => self.edits.push(CreateElementNs { id: id.0, ns, tag }), + None => self.edits.push(CreateElement { id: id.0, tag }), + } + } // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom - fn create_placeholder(&mut self) -> RealDomNode; + pub(crate) fn create_placeholder(&mut self, id: RealDomNode) { + self.edits.push(CreatePlaceholder { id: id.0 }); + } // events - fn new_event_listener( + pub(crate) fn new_event_listener( &mut self, event: &'static str, scope: ScopeIdx, element_id: usize, realnode: RealDomNode, - ); - fn remove_event_listener(&mut self, event: &'static str); + ) { + self.edits.push(NewEventListener { + scope, + event, + idx: element_id, + node: realnode.0, + }); + } + pub(crate) fn remove_event_listener(&mut self, event: &'static str) { + self.edits.push(RemoveEventListener { event }); + } // modify - fn set_text(&mut self, text: &'a str); - fn set_attribute(&mut self, name: &'static str, value: &'a str, ns: Option<&'a str>); - fn remove_attribute(&mut self, name: &'static str); - - // node ref - fn raw_node_as_any_mut(&self) -> &mut dyn Any; + pub(crate) fn set_text(&mut self, text: &'bump str) { + self.edits.push(SetText { text }); + } + pub(crate) fn set_attribute( + &mut self, + field: &'static str, + value: &'bump str, + ns: Option<&'static str>, + ) { + self.edits.push(SetAttribute { field, value, ns }); + } + pub(crate) fn remove_attribute(&mut self, name: &'static str) { + self.edits.push(RemoveAttribute { name }); + } } pub struct DiffMachine<'real, 'bump, Dom: RealDom<'bump>> { pub dom: &'real mut Dom, + pub edits: DomEditor<'real, 'bump>, pub components: &'bump SharedArena, pub task_queue: &'bump TaskQueue, pub cur_idx: ScopeIdx, @@ -122,6 +176,7 @@ where Dom: RealDom<'bump>, { pub fn new( + edits: &'real mut Vec>, dom: &'real mut Dom, components: &'bump SharedArena, cur_idx: ScopeIdx, @@ -129,6 +184,7 @@ where task_queue: &'bump TaskQueue, ) -> Self { Self { + edits: DomEditor { edits }, components, dom, cur_idx, @@ -153,10 +209,10 @@ where (VNodeKind::Text(old), VNodeKind::Text(new)) => { let root = old_node.dom_id.get(); if old.text != new.text { - self.dom.push(root); + self.edits.push(root); log::debug!("Text has changed {}, {}", old.text, new.text); - self.dom.set_text(new.text); - self.dom.pop(); + self.edits.set_text(new.text); + self.edits.pop(); } new_node.dom_id.set(root); @@ -169,10 +225,10 @@ where // In Dioxus, this is less likely to occur unless through a fragment let root = old_node.dom_id.get(); if new.tag_name != old.tag_name || new.namespace != old.namespace { - self.dom.push(root); + self.edits.push(root); let meta = self.create(new_node); - self.dom.replace_with(meta.added_to_stack); - self.dom.pop(); + self.edits.replace_with(meta.added_to_stack); + self.edits.pop(); return; } @@ -180,11 +236,11 @@ where // push it just in case // TODO: remove this - it clogs up things and is inefficient - self.dom.push(root); + self.edits.push(root); self.diff_listeners(old.listeners, new.listeners); self.diff_attr(old.attributes, new.attributes, new.namespace); self.diff_children(old.children, new.children); - self.dom.pop(); + self.edits.pop(); } (VNodeKind::Component(old), VNodeKind::Component(new)) => { @@ -227,15 +283,15 @@ where // remove any leftovers for to_remove in old_iter { - self.dom.push(to_remove); - self.dom.remove(); + self.edits.push(to_remove); + self.edits.remove(); } // seems like we could combine this into a single instruction.... - self.dom.push(first); + self.edits.push(first); let meta = self.create(new_node); - self.dom.replace_with(meta.added_to_stack); - self.dom.pop(); + self.edits.replace_with(meta.added_to_stack); + self.edits.pop(); // Wipe the old one and plant the new one let old_scope = old.ass_scope.get().unwrap(); @@ -287,8 +343,8 @@ where // remove any leftovers for to_remove in old_iter { - self.dom.push(to_remove); - self.dom.remove(); + self.edits.push(to_remove); + self.edits.remove(); } back_node @@ -296,9 +352,9 @@ where }; // replace the placeholder or first node with the nodes generated from the "new" - self.dom.push(back_node); + self.edits.push(back_node); let meta = self.create(new_node); - self.dom.replace_with(meta.added_to_stack); + self.edits.replace_with(meta.added_to_stack); // todo use the is_static metadata to update this subtree } @@ -343,7 +399,8 @@ where log::warn!("Creating node! ... {:#?}", node); match &node.kind { VNodeKind::Text(text) => { - let real_id = self.dom.create_text_node(text.text); + let real_id = self.dom.request_available_node(); + self.edits.create_text_node(text.text, real_id); node.dom_id.set(real_id); CreateMeta::new(text.is_static, 1) } @@ -366,17 +423,19 @@ where static_listeners: _, } = el; - let real_id = if let Some(namespace) = namespace { - self.dom.create_element(tag_name, Some(namespace)) + let real_id = self.dom.request_available_node(); + if let Some(namespace) = namespace { + self.edits + .create_element(tag_name, Some(namespace), real_id) } else { - self.dom.create_element(tag_name, None) + self.edits.create_element(tag_name, None, real_id) }; node.dom_id.set(real_id); listeners.iter().enumerate().for_each(|(idx, listener)| { log::info!("setting listener id to {:#?}", real_id); listener.mounted_node.set(real_id); - self.dom + self.edits .new_event_listener(listener.event, listener.scope, idx, real_id); // if the node has an event listener, then it must be visited ? @@ -385,7 +444,8 @@ where for attr in *attributes { is_static = is_static && attr.is_static; - self.dom.set_attribute(&attr.name, &attr.value, *namespace); + self.edits + .set_attribute(&attr.name, &attr.value, *namespace); } // Fast path: if there is a single text child, it is faster to @@ -400,7 +460,7 @@ where // TODO move over // if children.len() == 1 { // if let VNodeKind::Text(text) = &children[0].kind { - // self.dom.set_text(text.text); + // self.edits.set_text(text.text); // return CreateMeta::new(is_static, 1); // } // } @@ -410,7 +470,7 @@ where is_static = is_static && child_meta.is_static; // append whatever children were generated by this call - self.dom.append_children(child_meta.added_to_stack); + self.edits.append_children(child_meta.added_to_stack); } // if is_static { @@ -500,7 +560,8 @@ where } VNodeKind::Suspended => { - let id = self.dom.create_placeholder(); + let id = self.dom.request_available_node(); + self.edits.create_placeholder(id); node.dom_id.set(id); CreateMeta::new(false, 1) } @@ -550,7 +611,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // The change list stack is left unchanged. fn diff_listeners(&mut self, old: &[Listener<'_>], new: &[Listener<'_>]) { if !old.is_empty() || !new.is_empty() { - // self.dom.commit_traversal(); + // self.edits.commit_traversal(); } // TODO // what does "diffing listeners" even mean? @@ -567,9 +628,9 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { if new_l.event == old_l.event { new_l.mounted_node.set(old_l.mounted_node.get()); // if new_l.id != old_l.id { - // self.dom.remove_event_listener(event_type); + // self.edits.remove_event_listener(event_type); // // TODO! we need to mess with events and assign them by RealDomNode - // // self.dom + // // self.edits // // .update_event_listener(event_type, new_l.scope, new_l.id) // } @@ -577,7 +638,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { } } - // self.dom + // self.edits // .new_event_listener(event_type, new_l.scope, new_l.id); } @@ -587,7 +648,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // continue 'outer2; // } // } - // self.dom.remove_event_listener(old_l.event); + // self.edits.remove_event_listener(old_l.event); // } } @@ -602,7 +663,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { &mut self, old: &'bump [Attribute<'bump>], new: &'bump [Attribute<'bump>], - namespace: Option<&'bump str>, + namespace: Option<&'static str>, ) { // Do O(n^2) passes to add/update and remove attributes, since // there are almost always very few attributes. @@ -611,15 +672,15 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // With the Rsx and Html macros, this will almost always be the case 'outer: for new_attr in new { if new_attr.is_volatile { - // self.dom.commit_traversal(); - self.dom + // self.edits.commit_traversal(); + self.edits .set_attribute(new_attr.name, new_attr.value, namespace); } else { for old_attr in old { if old_attr.name == new_attr.name { if old_attr.value != new_attr.value { - // self.dom.commit_traversal(); - self.dom + // self.edits.commit_traversal(); + self.edits .set_attribute(new_attr.name, new_attr.value, namespace); } continue 'outer; @@ -628,8 +689,8 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { } } - // self.dom.commit_traversal(); - self.dom + // self.edits.commit_traversal(); + self.edits .set_attribute(new_attr.name, new_attr.value, namespace); } } @@ -641,8 +702,8 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { } } - // self.dom.commit_traversal(); - self.dom.remove_attribute(old_attr.name); + // self.edits.commit_traversal(); + self.edits.remove_attribute(old_attr.name); } } @@ -657,7 +718,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { fn diff_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { if new.is_empty() { if !old.is_empty() { - // self.dom.commit_traversal(); + // self.edits.commit_traversal(); self.remove_all_children(old); } return; @@ -672,9 +733,9 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // } // (_, VNodeKind::Text(text)) => { - // // self.dom.commit_traversal(); + // // self.edits.commit_traversal(); // log::debug!("using optimized text set"); - // self.dom.set_text(text.text); + // self.edits.set_text(text.text); // return; // } @@ -685,7 +746,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { if old.is_empty() { if !new.is_empty() { - // self.dom.commit_traversal(); + // self.edits.commit_traversal(); self.create_and_append_children(new); } return; @@ -707,9 +768,9 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { log::warn!("using the wrong approach"); self.diff_non_keyed_children(old, new); // todo!("Not yet implemented a migration away from temporaries"); - // let t = self.dom.next_temporary(); + // let t = self.edits.next_temporary(); // self.diff_keyed_children(old, new); - // self.dom.set_next_temporary(t); + // self.edits.set_next_temporary(t); } else { // log::debug!("diffing non keyed children"); self.diff_non_keyed_children(old, new); @@ -827,7 +888,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { _new: &'bump [VNode<'bump>], ) -> KeyedPrefixResult { todo!() - // self.dom.go_down(); + // self.edits.go_down(); // let mut shared_prefix_count = 0; // for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() { @@ -835,7 +896,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // break; // } - // self.dom.go_to_sibling(i); + // self.edits.go_to_sibling(i); // self.diff_node(old, new); @@ -845,8 +906,8 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // // If that was all of the old children, then create and append the remaining // // new children and we're finished. // if shared_prefix_count == old.len() { - // self.dom.go_up(); - // // self.dom.commit_traversal(); + // self.edits.go_up(); + // // self.edits.commit_traversal(); // self.create_and_append_children(&new[shared_prefix_count..]); // return KeyedPrefixResult::Finished; // } @@ -854,13 +915,13 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // // And if that was all of the new children, then remove all of the remaining // // old children and we're finished. // if shared_prefix_count == new.len() { - // self.dom.go_to_sibling(shared_prefix_count); - // // self.dom.commit_traversal(); + // self.edits.go_to_sibling(shared_prefix_count); + // // self.edits.commit_traversal(); // self.remove_self_and_next_siblings(&old[shared_prefix_count..]); // return KeyedPrefixResult::Finished; // } - // self.dom.go_up(); + // self.edits.go_up(); // KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) } @@ -872,7 +933,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // // When this function returns, the change list stack is in the same state. pub fn remove_all_children(&mut self, old: &'bump [VNode<'bump>]) { - // debug_assert!(self.dom.traversal_is_committed()); + // debug_assert!(self.edits.traversal_is_committed()); log::debug!("REMOVING CHILDREN"); for _child in old { // registry.remove_subtree(child); @@ -880,7 +941,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // Fast way to remove all children: set the node's textContent to an empty // string. todo!() - // self.dom.set_inner_text(""); + // self.edits.set_inner_text(""); } // Create the given children and append them to the parent node. @@ -893,7 +954,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { pub fn create_and_append_children(&mut self, new: &'bump [VNode<'bump>]) { for child in new { let meta = self.create(child); - self.dom.append_children(meta.added_to_stack); + self.edits.append_children(meta.added_to_stack); } } @@ -955,11 +1016,11 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // // afresh. // if shared_suffix_count == 0 && shared_keys.is_empty() { // if shared_prefix_count == 0 { - // // self.dom.commit_traversal(); + // // self.edits.commit_traversal(); // self.remove_all_children(old); // } else { - // self.dom.go_down_to_child(shared_prefix_count); - // // self.dom.commit_traversal(); + // self.edits.go_down_to_child(shared_prefix_count); + // // self.edits.commit_traversal(); // self.remove_self_and_next_siblings(&old[shared_prefix_count..]); // } @@ -981,8 +1042,8 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // .unwrap_or(old.len()); // if end - start > 0 { - // // self.dom.commit_traversal(); - // let mut t = self.dom.save_children_to_temporaries( + // // self.edits.commit_traversal(); + // let mut t = self.edits.save_children_to_temporaries( // shared_prefix_count + start, // shared_prefix_count + end, // ); @@ -1007,8 +1068,8 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // if !shared_keys.contains(&old_child.key()) { // // registry.remove_subtree(old_child); // // todo - // // self.dom.commit_traversal(); - // self.dom.remove_child(i + shared_prefix_count); + // // self.edits.commit_traversal(); + // self.edits.remove_child(i + shared_prefix_count); // removed_count += 1; // } // } @@ -1051,7 +1112,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // // shared suffix to the change list stack. // // // // [... parent] - // self.dom + // self.edits // .go_down_to_child(old_shared_suffix_start - removed_count); // // [... parent first_child_of_shared_suffix] // } else { @@ -1067,29 +1128,29 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // let old_index = new_index_to_old_index[last_index]; // let temp = old_index_to_temp[old_index]; // // [... parent] - // self.dom.go_down_to_temp_child(temp); + // self.edits.go_down_to_temp_child(temp); // // [... parent last] // self.diff_node(&old[old_index], last); // if new_index_is_in_lis.contains(&last_index) { // // Don't move it, since it is already where it needs to be. // } else { - // // self.dom.commit_traversal(); + // // self.edits.commit_traversal(); // // [... parent last] - // self.dom.append_child(); + // self.edits.append_child(); // // [... parent] - // self.dom.go_down_to_temp_child(temp); + // self.edits.go_down_to_temp_child(temp); // // [... parent last] // } // } else { - // // self.dom.commit_traversal(); + // // self.edits.commit_traversal(); // // [... parent] // self.create(last); // // [... parent last] - // self.dom.append_child(); + // self.edits.append_child(); // // [... parent] - // self.dom.go_down_to_reverse_child(0); + // self.edits.go_down_to_reverse_child(0); // // [... parent last] // } // } @@ -1098,11 +1159,11 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // let old_index = new_index_to_old_index[new_index]; // if old_index == u32::MAX as usize { // debug_assert!(!shared_keys.contains(&new_child.key())); - // // self.dom.commit_traversal(); + // // self.edits.commit_traversal(); // // [... parent successor] // self.create(new_child); // // [... parent successor new_child] - // self.dom.insert_before(); + // self.edits.insert_before(); // // [... parent new_child] // } else { // debug_assert!(shared_keys.contains(&new_child.key())); @@ -1111,14 +1172,14 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // if new_index_is_in_lis.contains(&new_index) { // // [... parent successor] - // self.dom.go_to_temp_sibling(temp); + // self.edits.go_to_temp_sibling(temp); // // [... parent new_child] // } else { - // // self.dom.commit_traversal(); + // // self.edits.commit_traversal(); // // [... parent successor] - // self.dom.push_temporary(temp); + // self.edits.push_temporary(temp); // // [... parent successor new_child] - // self.dom.insert_before(); + // self.edits.insert_before(); // // [... parent new_child] // } @@ -1127,7 +1188,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // } // // [... parent child] - // self.dom.go_up(); + // self.edits.go_up(); // [... parent] } @@ -1149,16 +1210,16 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // debug_assert!(!old.is_empty()); // // [... parent] - // self.dom.go_down(); + // self.edits.go_down(); // // [... parent new_child] // for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() { - // self.dom.go_to_sibling(new_shared_suffix_start + i); + // self.edits.go_to_sibling(new_shared_suffix_start + i); // self.diff_node(old_child, new_child); // } // // [... parent] - // self.dom.go_up(); + // self.edits.go_up(); } // Diff children that are not keyed. @@ -1175,21 +1236,21 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { debug_assert!(!old.is_empty()); // [... parent] - // self.dom.go_down(); - // self.dom.push_root() + // self.edits.go_down(); + // self.edits.push_root() // [... parent child] // todo!() for (_i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() { // [... parent prev_child] - // self.dom.go_to_sibling(i); + // self.edits.go_to_sibling(i); // [... parent this_child] // let did = old_child.get_mounted_id(self.components).unwrap(); // if did.0 == 0 { // log::debug!("Root is bad: {:#?}", old_child); // } - // self.dom.push_root(did); + // self.edits.push_root(did); self.diff_node(old_child, new_child); // let old_id = old_child.get_mounted_id(self.components).unwrap(); @@ -1209,9 +1270,9 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // // old.len > new.len -> removing some nodes // Ordering::Greater => { // // [... parent prev_child] - // self.dom.go_to_sibling(new.len()); + // self.edits.go_to_sibling(new.len()); // // [... parent first_child_to_remove] - // // self.dom.commit_traversal(); + // // self.edits.commit_traversal(); // // support::remove_self_and_next_siblings(state, &old[new.len()..]); // self.remove_self_and_next_siblings(&old[new.len()..]); // // [... parent] @@ -1219,15 +1280,15 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // // old.len < new.len -> adding some nodes // Ordering::Less => { // // [... parent last_child] - // self.dom.go_up(); + // self.edits.go_up(); // // [... parent] - // // self.dom.commit_traversal(); + // // self.edits.commit_traversal(); // self.create_and_append_children(&new[old.len()..]); // } // // old.len == new.len -> no nodes added/removed, but πerhaps changed // Ordering::Equal => { // // [... parent child] - // self.dom.go_up(); + // self.edits.go_up(); // // [... parent] // } // } @@ -1247,7 +1308,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // // [... parent] pub fn remove_self_and_next_siblings(&self, old: &[VNode<'bump>]) { - // debug_assert!(self.dom.traversal_is_committed()); + // debug_assert!(self.edits.traversal_is_committed()); for child in old { if let VNodeKind::Component(_vcomp) = child.kind { // dom @@ -1261,7 +1322,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // }) // let id = get_id(); // *component.stable_addr.as_ref().borrow_mut() = Some(id); - // self.dom.save_known_root(id); + // self.edits.save_known_root(id); // let scope = Rc::downgrade(&component.ass_scope); // self.lifecycle_events.push_back(LifeCycleEvent::Mount { // caller: Rc::downgrade(&component.caller), @@ -1273,7 +1334,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // registry.remove_subtree(child); } todo!() - // self.dom.remove_self_and_next_siblings(); + // self.edits.remove_self_and_next_siblings(); } } diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index 9b4ee7793..0b4bc9b7f 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -88,52 +88,13 @@ impl DebugDom { } } impl<'a> RealDom<'a> for DebugDom { - fn push(&mut self, _root: RealDomNode) {} - fn pop(&mut self) {} - - fn append_children(&mut self, _many: u32) {} - - fn replace_with(&mut self, _many: u32) {} - - fn remove(&mut self) {} - - fn remove_all_children(&mut self) {} - - fn create_text_node(&mut self, _text: &str) -> RealDomNode { - self.counter += 1; - RealDomNode::new(self.counter) - } - - fn create_element(&mut self, _tag: &str, _ns: Option<&'a str>) -> RealDomNode { - self.counter += 1; - RealDomNode::new(self.counter) - } - - fn create_placeholder(&mut self) -> RealDomNode { - self.counter += 1; - RealDomNode::new(self.counter) - } - - fn new_event_listener( - &mut self, - _event: &str, - _scope: ScopeIdx, - _element_id: usize, - _realnode: RealDomNode, - ) { - } - - fn remove_event_listener(&mut self, _event: &str) {} - - fn set_text(&mut self, _text: &str) {} - - fn set_attribute(&mut self, _name: &str, _value: &str, _namespace: Option<&str>) {} - - fn remove_attribute(&mut self, _name: &str) {} - fn raw_node_as_any_mut(&self) -> &mut dyn std::any::Any { todo!() } + + fn request_available_node(&mut self) -> RealDomNode { + todo!() + } } async fn launch_demo(app: FC<()>) { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index b98ebbccb..4cc700773 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -27,6 +27,7 @@ use slotmap::SlotMap; use std::any::Any; use std::any::TypeId; +use std::cell::RefCell; use std::pin::Pin; pub type ScopeIdx = DefaultKey; @@ -45,6 +46,8 @@ pub struct VirtualDom { /// Should always be the first (gen=0, id=0) pub base_scope: ScopeIdx, + pub triggers: RefCell>, + /// All components dump their updates into a queue to be processed pub event_queue: EventQueue, @@ -158,6 +161,7 @@ impl VirtualDom { components, root_props, tasks, + triggers: Default::default(), _root_prop_type: TypeId::of::

(), } } @@ -196,7 +200,9 @@ impl VirtualDom { /// /// The diff machine expects the RealDom's stack to be the root of the application pub fn rebuild<'s, Dom: RealDom<'s>>(&'s mut self, realdom: &mut Dom) -> Result<()> { + let mut edits = Vec::new(); let mut diff_machine = DiffMachine::new( + &mut edits, realdom, &self.components, self.base_scope, @@ -209,24 +215,23 @@ impl VirtualDom { cur_component.run_scope()?; let meta = diff_machine.create(cur_component.next_frame()); - log::info!( - "nodes created! appending to body {:#?}", - meta.added_to_stack - ); - diff_machine.dom.append_children(meta.added_to_stack); - // Schedule an update and then immediately call it on the root component - // This is akin to a hook being called from a listener and requring a re-render - // Instead, this is done on top-level component - // let base = self.components.try_get(self.base_scope)?; - - // let update = &base.event_channel; - // update(); - - // self.progress_completely(&mut diff_machine)?; + diff_machine.edits.append_children(meta.added_to_stack); Ok(()) } + + /// + /// + /// + /// + /// + pub fn queue_event(&self, trigger: EventTrigger) -> Result<()> { + let mut triggers = self.triggers.borrow_mut(); + triggers.push(trigger); + Ok(()) + } + /// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered. /// /// Given a synthetic event, the component that triggered the event, and the index of the callback, this runs the virtual @@ -270,74 +275,78 @@ impl VirtualDom { // but the guarantees provide a safe, fast, and efficient abstraction for the VirtualDOM updating framework. // // A good project would be to remove all unsafe from this crate and move the unsafety into safer abstractions. - pub fn progress_with_event<'s, Dom: RealDom<'s>>( + pub async fn progress_with_event<'s, Dom: RealDom<'s>>( &'s mut self, realdom: &'_ mut Dom, - trigger: EventTrigger, ) -> Result<()> { - let id = trigger.originator.clone(); - - self.components.try_get_mut(id)?.call_listener(trigger)?; + let trigger = self.triggers.borrow_mut().pop().expect("failed"); + let mut edits = Vec::new(); let mut diff_machine = DiffMachine::new( + &mut edits, realdom, &self.components, - id, + trigger.originator, self.event_queue.clone(), &self.tasks, ); - self.progress_completely(&mut diff_machine)?; + match &trigger.event { + VirtualEvent::OtherEvent => todo!(), - Ok(()) - } - - /// Consume the event queue, descending depth-first. - /// Only ever run each component once. - /// - /// The DiffMachine logs its progress as it goes which might be useful for certain types of renderers. - pub(crate) fn progress_completely<'a, 'bump, Dom: RealDom<'bump>>( - &'bump self, - diff_machine: &'_ mut DiffMachine<'a, 'bump, Dom>, - ) -> Result<()> { - // Now, there are events in the queue - let mut updates = self.event_queue.queue.as_ref().borrow_mut(); - - // Order the nodes by their height, we want the nodes with the smallest depth on top - // This prevents us from running the same component multiple times - updates.sort_unstable(); - - log::debug!("There are: {:#?} updates to be processed", updates.len()); - - // Iterate through the triggered nodes (sorted by height) and begin to diff them - for update in updates.drain(..) { - log::debug!("Running updates for: {:#?}", update); - - // Make sure this isn't a node we've already seen, we don't want to double-render anything - // If we double-renderer something, this would cause memory safety issues - if diff_machine.seen_nodes.contains(&update.idx) { - continue; + // Fiber events + VirtualEvent::FiberEvent => { + // } - // Now, all the "seen nodes" are nodes that got notified by running this listener - diff_machine.seen_nodes.insert(update.idx.clone()); + // This is the "meat" of our cooperative scheduler + // As updates flow in, we re-evalute the event queue and decide if we should be switching the type of work + // + // We use the reconciler to request new IDs and then commit/uncommit the IDs when the scheduler is finished + _ => { + self.components + .try_get_mut(trigger.originator)? + .call_listener(trigger)?; - // Start a new mutable borrow to components - // We are guaranteeed that this scope is unique because we are tracking which nodes have modified - let cur_component = self.components.try_get_mut(update.idx).unwrap(); + // Now, there are events in the queue + let mut updates = self.event_queue.queue.as_ref().borrow_mut(); - cur_component.run_scope()?; + // Order the nodes by their height, we want the nodes with the smallest depth on top + // This prevents us from running the same component multiple times + updates.sort_unstable(); - let (old, new) = (cur_component.old_frame(), cur_component.next_frame()); - diff_machine.diff_node(old, new); + log::debug!("There are: {:#?} updates to be processed", updates.len()); + + // Iterate through the triggered nodes (sorted by height) and begin to diff them + for update in updates.drain(..) { + log::debug!("Running updates for: {:#?}", update); + + // Make sure this isn't a node we've already seen, we don't want to double-render anything + // If we double-renderer something, this would cause memory safety issues + if diff_machine.seen_nodes.contains(&update.idx) { + continue; + } + + // Now, all the "seen nodes" are nodes that got notified by running this listener + diff_machine.seen_nodes.insert(update.idx.clone()); + + // Start a new mutable borrow to components + // We are guaranteeed that this scope is unique because we are tracking which nodes have modified + let cur_component = self.components.try_get_mut(update.idx).unwrap(); + + cur_component.run_scope()?; + + let (old, new) = (cur_component.old_frame(), cur_component.next_frame()); + diff_machine.diff_node(old, new); + } + } } Ok(()) } pub fn base_scope(&self) -> &Scope { - let idx = self.base_scope; - self.components.try_get(idx).unwrap() + self.components.try_get(self.base_scope).unwrap() } } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 3dd128c20..55c98a604 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -100,8 +100,11 @@ impl WebsysRenderer { // let root_node = body_element.first_child().unwrap(); // websys_dom.stack.push(root_node.clone()); + self.internal_dom.queue_event(real_trigger)?; + self.internal_dom - .progress_with_event(&mut websys_dom, real_trigger)?; + .progress_with_event(&mut websys_dom) + .await?; } // let t2 = self.internal_dom.tasks.next(); diff --git a/packages/webview/src/dom.rs b/packages/webview/src/dom.rs index 7079eca33..8f4cf0f29 100644 --- a/packages/webview/src/dom.rs +++ b/packages/webview/src/dom.rs @@ -31,86 +31,12 @@ impl WebviewDom<'_> { } } impl<'bump> RealDom<'bump> for WebviewDom<'bump> { - 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 }); - } - - fn replace_with(&mut self, many: u32) { - self.edits.push(ReplaceWith { many }); - } - - fn remove(&mut self) { - self.edits.push(Remove); - } - - fn remove_all_children(&mut self) { - self.edits.push(RemoveAllChildren); - } - - fn create_text_node(&mut self, text: &'bump str) -> RealDomNode { - self.node_counter += 1; - let id = RealDomNode::new(self.node_counter); - self.edits.push(CreateTextNode { text, id: id.0 }); - id - } - - fn create_element(&mut self, tag: &'bump str, ns: Option<&'bump str>) -> RealDomNode { - self.node_counter += 1; - let id = RealDomNode::new(self.node_counter); - match ns { - Some(ns) => self.edits.push(CreateElementNs { id: id.0, ns, tag }), - None => self.edits.push(CreateElement { id: id.0, tag }), - } - id - } - - fn create_placeholder(&mut self) -> RealDomNode { - self.node_counter += 1; - let id = RealDomNode::new(self.node_counter); - self.edits.push(CreatePlaceholder { id: id.0 }); - id - } - - fn new_event_listener( - &mut self, - event: &'static str, - scope: ScopeIdx, - element_id: usize, - realnode: RealDomNode, - ) { - self.edits.push(NewEventListener { - scope, - event, - idx: element_id, - node: realnode.0, - }); - } - - fn remove_event_listener(&mut self, event: &'static str) { - self.edits.push(RemoveEventListener { event }); - } - - fn set_text(&mut self, text: &'bump str) { - self.edits.push(SetText { text }); - } - - fn set_attribute(&mut self, field: &'static str, value: &'bump str, ns: Option<&'bump str>) { - self.edits.push(SetAttribute { field, value, ns }); - } - - fn remove_attribute(&mut self, name: &'static str) { - self.edits.push(RemoveAttribute { name }); - } - fn raw_node_as_any_mut(&self) -> &mut dyn std::any::Any { todo!() // self.edits.push(PushRoot { root }); } + + fn request_available_node(&mut self) -> RealDomNode { + todo!() + } }