Răsfoiți Sursa

feat: suspense!

Jonathan Kelley 4 ani în urmă
părinte
comite
4837d8e

+ 2 - 2
README.md

@@ -163,13 +163,13 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an
 | Controlled Inputs       | ✅      | ✅     | stateful wrappers around inputs                             |
 | CSS/Inline Styles       | ✅      | ✅     | syntax for inline styles/attribute groups                   |
 | Custom elements         | ✅      | ✅     | Define new element primitives                               |
-| Suspense                | 🛠      | ✅     | schedule future render from future/promise                  |
+| Suspense                |       | ✅     | schedule future render from future/promise                  |
 | Cooperative Scheduling  | 🛠      | ✅     | Prioritize important events over non-important events       |
-| Fine-grained reactivity | 🛠      | ❓     | Skip diffing for fine-grain updates                         |
 | Runs natively           | ✅      | ❓     | runs as a portable binary w/o a runtime (Node)              |
 | 1st class global state  | ✅      | ❓     | redux/recoil/mobx on top of context                         |
 | Subtree Memoization     | ✅      | ❓     | skip diffing static element subtrees                        |
 | Compile-time correct    | ✅      | ❓     | Throw errors on invalid template layouts                    |
+| Fine-grained reactivity | 🛠      | ❓     | Skip diffing for fine-grain updates                         |
 | Heuristic Engine        | 🛠      | ❓     | track component memory usage to minimize future allocations |
 | NodeRef                 | 🛠      | ✅     | gain direct access to nodes [1]                             |
 

+ 39 - 30
packages/core/src/context.rs

@@ -4,6 +4,7 @@ use bumpalo::Bump;
 use futures_util::FutureExt;
 
 use std::any::Any;
+use std::cell::Cell;
 use std::marker::PhantomData;
 
 use std::{
@@ -355,8 +356,9 @@ Any function prefixed with "use" should not be called conditionally.
 pub(crate) struct SuspenseHook {
     pub value: Rc<RefCell<Option<Box<dyn Any>>>>,
     pub callback: SuspendedCallback,
+    pub dom_node_id: Rc<Cell<RealDomNode>>,
 }
-type SuspendedCallback = Box<dyn for<'a> Fn(SuspendedContext<'a>) -> VNode<'a>>;
+type SuspendedCallback = Box<dyn for<'a> Fn(Context<'a, ()>) -> VNode<'a>>;
 
 impl<'src, P> Context<'src, P> {
     /// Asynchronously render new nodes once the given future has completed.
@@ -370,29 +372,48 @@ impl<'src, P> Context<'src, P> {
     ///
     ///
     pub fn use_suspense<Out, Fut, Cb>(
-        &'src self,
+        self,
         task_initializer: impl FnOnce() -> Fut,
         user_callback: Cb,
     ) -> VNode<'src>
     where
         Fut: Future<Output = Out> + 'static,
         Out: 'static,
-        Cb: for<'a> Fn(SuspendedContext<'a>, &Out) -> VNode<'a> + 'static,
+        Cb: for<'a> Fn(Context<'a, ()>, &Out) -> VNode<'a> + 'static,
     {
         self.use_hook(
             move |hook_idx| {
                 let value = Rc::new(RefCell::new(None));
 
+                let dom_node_id = Rc::new(RealDomNode::empty_cell());
+                let domnode = dom_node_id.clone();
+
                 let slot = value.clone();
-                let callback: SuspendedCallback = Box::new(move |ctx: SuspendedContext| {
+
+                let callback: SuspendedCallback = Box::new(move |ctx: Context<()>| {
                     let v: std::cell::Ref<Option<Box<dyn Any>>> = slot.as_ref().borrow();
-                    let v: &dyn Any = v.as_ref().unwrap().as_ref();
-                    let real_val = v.downcast_ref::<Out>().unwrap();
-                    user_callback(ctx, real_val)
+                    match v.as_ref() {
+                        Some(a) => {
+                            let v: &dyn Any = a.as_ref();
+                            let real_val = v.downcast_ref::<Out>().unwrap();
+                            user_callback(ctx, real_val)
+                        }
+                        None => {
+                            //
+                            VNode {
+                                dom_id: RealDomNode::empty_cell(),
+                                key: None,
+                                kind: VNodeKind::Suspended {
+                                    node: domnode.clone(),
+                                },
+                            }
+                        }
+                    }
                 });
 
                 let originator = self.scope.arena_idx.clone();
                 let task_fut = task_initializer();
+                let domnode = dom_node_id.clone();
 
                 let slot = value.clone();
                 self.submit_task(Box::pin(task_fut.then(move |output| async move {
@@ -400,33 +421,25 @@ impl<'src, P> Context<'src, P> {
                     // Dioxus will call the user's callback to generate new nodes outside of the diffing system
                     *slot.borrow_mut() = Some(Box::new(output) as Box<dyn Any>);
                     EventTrigger {
-                        event: VirtualEvent::SuspenseEvent { hook_idx },
+                        event: VirtualEvent::SuspenseEvent { hook_idx, domnode },
                         originator,
                         priority: EventPriority::Low,
                         real_node_id: None,
                     }
                 })));
 
-                SuspenseHook { value, callback }
+                SuspenseHook {
+                    value,
+                    callback,
+                    dom_node_id,
+                }
             },
             move |hook| {
-                match hook.value.borrow().as_ref() {
-                    Some(val) => {
-                        let cx = SuspendedContext {
-                            bump: &self.scope.cur_frame().bump,
-                        };
-                        (&hook.callback)(cx)
-                    }
-                    None => {
-                        //
-                        VNode {
-                            dom_id: RealDomNode::empty_cell(),
-                            key: None,
-                            kind: VNodeKind::Suspended,
-                        }
-                    }
-                }
-                //
+                let cx = Context {
+                    scope: &self.scope,
+                    props: &(),
+                };
+                (&hook.callback)(cx)
             },
             |_| {},
         )
@@ -436,7 +449,3 @@ impl<'src, P> Context<'src, P> {
 pub struct TaskHandle<'src> {
     _p: PhantomData<&'src ()>,
 }
-#[derive(Clone)]
-pub struct SuspendedContext<'a> {
-    pub bump: &'a Bump,
-}

+ 9 - 99
packages/core/src/diff.rs

@@ -70,100 +70,6 @@ pub trait RealDom<'a> {
     fn raw_node_as_any(&self) -> &mut dyn Any;
 }
 
-pub struct DomEditor<'real, 'bump> {
-    edits: &'real mut Vec<DomEdit<'bump>>,
-}
-use DomEdit::*;
-impl<'real, 'bump> DomEditor<'real, 'bump> {
-    // Navigation
-    pub(crate) fn push(&mut self, root: RealDomNode) {
-        let id = root.as_u64();
-        self.edits.push(PushRoot { id });
-    }
-    pub(crate) fn pop(&mut self) {
-        self.edits.push(PopRoot {});
-    }
-
-    // Add Nodes to the dom
-    // add m nodes from the stack
-    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
-    pub(crate) fn replace_with(&mut self, many: u32) {
-        self.edits.push(ReplaceWith { many });
-    }
-
-    // Remove Nodesfrom the dom
-    pub(crate) fn remove(&mut self) {
-        self.edits.push(Remove);
-    }
-    pub(crate) fn remove_all_children(&mut self) {
-        self.edits.push(RemoveAllChildren);
-    }
-
-    // Create
-    pub(crate) fn create_text_node(&mut self, text: &'bump str, id: RealDomNode) {
-        let id = id.as_u64();
-        self.edits.push(CreateTextNode { text, id });
-    }
-    pub(crate) fn create_element(
-        &mut self,
-        tag: &'static str,
-        ns: Option<&'static str>,
-        id: RealDomNode,
-    ) {
-        let id = id.as_u64();
-        match ns {
-            Some(ns) => self.edits.push(CreateElementNs { id, ns, tag }),
-            None => self.edits.push(CreateElement { id, tag }),
-        }
-    }
-
-    // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom
-    pub(crate) fn create_placeholder(&mut self, id: RealDomNode) {
-        let id = id.as_u64();
-        self.edits.push(CreatePlaceholder { id });
-    }
-
-    // events
-    pub(crate) 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.as_u64(),
-        });
-    }
-    pub(crate) fn remove_event_listener(&mut self, event: &'static str) {
-        self.edits.push(RemoveEventListener { event });
-    }
-
-    // modify
-    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>,
@@ -188,7 +94,7 @@ where
         task_queue: &'bump TaskQueue,
     ) -> Self {
         Self {
-            edits: DomEditor { edits },
+            edits: DomEditor::new(edits),
             components,
             dom,
             cur_idx,
@@ -364,8 +270,11 @@ where
             }
 
             // TODO
-            (VNodeKind::Suspended { .. }, _) => todo!(),
-            (_, VNodeKind::Suspended { .. }) => todo!(),
+            (VNodeKind::Suspended { node }, new) => todo!(),
+            (old, VNodeKind::Suspended { .. }) => {
+                // a node that was once real is now suspended
+                //
+            }
         }
     }
 }
@@ -563,10 +472,11 @@ where
                 CreateMeta::new(false, nodes_added)
             }
 
-            VNodeKind::Suspended => {
+            VNodeKind::Suspended { node: real_node } => {
                 let id = self.dom.request_available_node();
                 self.edits.create_placeholder(id);
                 node.dom_id.set(id);
+                real_node.set(id);
                 CreateMeta::new(false, 1)
             }
         }
@@ -1411,7 +1321,7 @@ impl<'a> Iterator for RealChildIterator<'a> {
 
                     // Immediately abort suspended nodes - can't do anything with them yet
                     // VNodeKind::Suspended => should_pop = true,
-                    VNodeKind::Suspended => todo!(),
+                    VNodeKind::Suspended { .. } => todo!(),
 
                     // For components, we load their root and push them onto the stack
                     VNodeKind::Component(sc) => {

+ 173 - 0
packages/core/src/editor.rs

@@ -0,0 +1,173 @@
+//! Serialization
+//! -------------
+//!
+//!
+//!
+//!
+//!
+//!
+
+use crate::{innerlude::ScopeIdx, RealDomNode};
+
+/// The `DomEditor` provides an imperative interface for the Diffing algorithm to plan out its changes.
+///
+/// However, the DomEditor only builds a change list - it does not apply them. In contrast with the "RealDom", the DomEditor
+/// is cancellable and flushable. At any moment in time, Dioxus may choose to completely clear the edit list and start over.
+///
+/// This behavior is used in the cooperative scheduling algorithm
+pub struct DomEditor<'real, 'bump> {
+    pub edits: &'real mut Vec<DomEdit<'bump>>,
+}
+use DomEdit::*;
+impl<'real, 'bump> DomEditor<'real, 'bump> {
+    pub fn new(edits: &'real mut Vec<DomEdit<'bump>>) -> Self {
+        Self { edits }
+    }
+
+    // Navigation
+    pub(crate) fn push(&mut self, root: RealDomNode) {
+        let id = root.as_u64();
+        self.edits.push(PushRoot { id });
+    }
+    pub(crate) fn pop(&mut self) {
+        self.edits.push(PopRoot {});
+    }
+
+    // Add Nodes to the dom
+    // add m nodes from the stack
+    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
+    pub(crate) fn replace_with(&mut self, many: u32) {
+        self.edits.push(ReplaceWith { many });
+    }
+
+    // Remove Nodesfrom the dom
+    pub(crate) fn remove(&mut self) {
+        self.edits.push(Remove);
+    }
+    pub(crate) fn remove_all_children(&mut self) {
+        self.edits.push(RemoveAllChildren);
+    }
+
+    // Create
+    pub(crate) fn create_text_node(&mut self, text: &'bump str, id: RealDomNode) {
+        let id = id.as_u64();
+        self.edits.push(CreateTextNode { text, id });
+    }
+    pub(crate) fn create_element(
+        &mut self,
+        tag: &'static str,
+        ns: Option<&'static str>,
+        id: RealDomNode,
+    ) {
+        let id = id.as_u64();
+        match ns {
+            Some(ns) => self.edits.push(CreateElementNs { id, ns, tag }),
+            None => self.edits.push(CreateElement { id, tag }),
+        }
+    }
+
+    // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom
+    pub(crate) fn create_placeholder(&mut self, id: RealDomNode) {
+        let id = id.as_u64();
+        self.edits.push(CreatePlaceholder { id });
+    }
+
+    // events
+    pub(crate) 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.as_u64(),
+        });
+    }
+    pub(crate) fn remove_event_listener(&mut self, event: &'static str) {
+        self.edits.push(RemoveEventListener { event });
+    }
+
+    // modify
+    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 });
+    }
+}
+
+/// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the
+/// network or through FFI boundaries.
+#[derive(Debug)]
+#[cfg_attr(
+    feature = "serialize",
+    derive(serde::Serialize, serde::Deserialize),
+    serde(tag = "type")
+)]
+pub enum DomEdit<'bump> {
+    PushRoot {
+        id: u64,
+    },
+    PopRoot,
+    AppendChildren {
+        many: u32,
+    },
+    ReplaceWith {
+        many: u32,
+    },
+    Remove,
+    RemoveAllChildren,
+    CreateTextNode {
+        text: &'bump str,
+        id: u64,
+    },
+    CreateElement {
+        tag: &'bump str,
+        id: u64,
+    },
+    CreateElementNs {
+        tag: &'bump str,
+        id: u64,
+        ns: &'static str,
+    },
+    CreatePlaceholder {
+        id: u64,
+    },
+    NewEventListener {
+        event: &'static str,
+        scope: ScopeIdx,
+        node: u64,
+        idx: usize,
+    },
+    RemoveEventListener {
+        event: &'static str,
+    },
+    SetText {
+        text: &'bump str,
+    },
+    SetAttribute {
+        field: &'static str,
+        value: &'bump str,
+        ns: Option<&'bump str>,
+    },
+    RemoveAttribute {
+        name: &'static str,
+    },
+}

+ 12 - 2
packages/core/src/events.rs

@@ -4,6 +4,11 @@
 //! 3rd party renderers are responsible for converting their native events into these virtual event types. Events might
 //! be heavy or need to interact through FFI, so the events themselves are designed to be lazy.
 
+use std::{
+    cell::{Cell, RefCell},
+    rc::Rc,
+};
+
 use crate::innerlude::{RealDomNode, ScopeIdx};
 
 #[derive(Debug)]
@@ -108,10 +113,15 @@ pub enum VirtualEvent {
     //
     // Async events don't necessarily propagate into a scope being ran. It's up to the event itself
     // to force an update for itself.
-    AsyncEvent { hook_idx: usize },
+    AsyncEvent {
+        hook_idx: usize,
+    },
 
     // These are more intrusive than the rest
-    SuspenseEvent { hook_idx: usize },
+    SuspenseEvent {
+        hook_idx: usize,
+        domnode: Rc<Cell<RealDomNode>>,
+    },
 
     OtherEvent,
 }

+ 2 - 3
packages/core/src/lib.rs

@@ -30,11 +30,11 @@ pub(crate) mod innerlude {
     pub use crate::component::*;
     pub use crate::context::*;
     pub use crate::diff::*;
+    pub use crate::editor::*;
     pub use crate::error::*;
     pub use crate::events::*;
     pub use crate::nodes::*;
     pub use crate::scope::*;
-    pub use crate::serialize::*;
     pub use crate::tasks::*;
     pub use crate::util::*;
     pub use crate::virtual_dom::*;
@@ -53,13 +53,12 @@ pub mod bumpframe;
 pub mod component;
 pub mod context;
 pub mod diff;
+pub mod editor;
 pub mod error;
 pub mod events;
 pub mod hooklist;
 pub mod nodes;
 pub mod scope;
-#[cfg(feature = "serialize")]
-pub mod serialize;
 pub mod signals;
 pub mod tasks;
 pub mod util;

+ 5 - 3
packages/core/src/nodes.rs

@@ -30,7 +30,7 @@ pub enum VNodeKind<'src> {
     Element(&'src VElement<'src>),
     Fragment(VFragment<'src>),
     Component(&'src VComponent<'src>),
-    Suspended,
+    Suspended { node: Rc<Cell<RealDomNode>> },
 }
 
 pub struct VText<'src> {
@@ -216,7 +216,9 @@ impl<'a> NodeFactory<'a> {
         VNode {
             dom_id: RealDomNode::empty_cell(),
             key: None,
-            kind: VNodeKind::Suspended,
+            kind: VNodeKind::Suspended {
+                node: Rc::new(RealDomNode::empty_cell()),
+            },
         }
     }
 
@@ -430,7 +432,7 @@ impl<'a> Clone for VNode<'a> {
                 is_static: fragment.is_static,
             }),
             VNodeKind::Component(component) => VNodeKind::Component(component),
-            VNodeKind::Suspended => VNodeKind::Suspended,
+            VNodeKind::Suspended { node } => VNodeKind::Suspended { node: node.clone() },
         };
         VNode {
             kind,

+ 0 - 66
packages/core/src/serialize.rs

@@ -1,66 +0,0 @@
-//! Serialization
-//! -------------
-//!
-//!
-//!
-//!
-//!
-//!
-
-use crate::innerlude::ScopeIdx;
-use serde::{Deserialize, Serialize};
-
-/// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the
-/// network or through FFI boundaries.
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(tag = "type")]
-pub enum DomEdit<'bump> {
-    PushRoot {
-        id: u64,
-    },
-    PopRoot,
-    AppendChildren {
-        many: u32,
-    },
-    ReplaceWith {
-        many: u32,
-    },
-    Remove,
-    RemoveAllChildren,
-    CreateTextNode {
-        text: &'bump str,
-        id: u64,
-    },
-    CreateElement {
-        tag: &'bump str,
-        id: u64,
-    },
-    CreateElementNs {
-        tag: &'bump str,
-        id: u64,
-        ns: &'static str,
-    },
-    CreatePlaceholder {
-        id: u64,
-    },
-    NewEventListener {
-        event: &'static str,
-        scope: ScopeIdx,
-        node: u64,
-        idx: usize,
-    },
-    RemoveEventListener {
-        event: &'static str,
-    },
-    SetText {
-        text: &'bump str,
-    },
-    SetAttribute {
-        field: &'static str,
-        value: &'bump str,
-        ns: Option<&'bump str>,
-    },
-    RemoveAttribute {
-        name: &'static str,
-    },
-}

+ 10 - 8
packages/core/src/virtual_dom.rs

@@ -301,24 +301,26 @@ impl VirtualDom {
             VirtualEvent::AsyncEvent { .. } => {}
 
             // Suspense Events! A component's suspended node is updated
-            VirtualEvent::SuspenseEvent { hook_idx } => {
+            VirtualEvent::SuspenseEvent { hook_idx, domnode } => {
                 let scope = self.components.try_get_mut(trigger.originator).unwrap();
 
                 // safety: we are sure that there are no other references to the inner content of this hook
                 let hook = unsafe { scope.hooks.get_mut::<SuspenseHook>(*hook_idx) }.unwrap();
 
-                let cx = SuspendedContext {
-                    bump: &scope.cur_frame().bump,
-                };
+                let cx = Context { scope, props: &() };
 
                 // generate the new node!
-                let callback: VNode<'s> = (&hook.callback)(cx);
+                let nodes: VNode<'s> = (&hook.callback)(cx);
+                let nodes = scope.cur_frame().bump.alloc(nodes);
 
-                // diff that node with the node that was originally suspended!
+                // push the old node's root onto the stack
+                diff_machine.edits.push(domnode.get());
 
-                // hook.callback;
+                // push these new nodes onto the diff machines stack
+                let meta = diff_machine.create(&*nodes);
 
-                //
+                // replace the placeholder with the new nodes we just pushed on the stack
+                diff_machine.edits.replace_with(meta.added_to_stack);
             }
 
             // This is the "meat" of our cooperative scheduler

+ 8 - 6
packages/web/examples/async_web.rs

@@ -34,13 +34,15 @@ const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random/";
 static App: FC<()> = |cx| {
     let state = use_state(cx, || 0);
 
-    let request = cx.use_task(|| surf::get(ENDPOINT).recv_json::<DogApi>()).1;
+    let dog_node = cx.use_suspense(
+        || surf::get(ENDPOINT).recv_json::<DogApi>(),
+        |cx, res| match res {
+            Ok(res) => rsx!(in cx, img { src: "{res.message}" }),
+            Err(err) => rsx!(in cx, div { "No doggos for you :(" }),
+        },
+    );
 
-    let dog_node = if let Some(Ok(res)) = request {
-        rsx!(in cx, img { src: "{res.message}" })
-    } else {
-        rsx!(in cx, div { "No doggos for you :(" })
-    };
+    log::error!("RIP WE RAN THE COMPONENT");
 
     cx.render(rsx! {
         div {