feat: suspense!

This commit is contained in:
Jonathan Kelley committed 2021-07-15 00:40:37 -04:00
1 parent de9f61bcf4
commit 4837d8e741
10 files changed
+260 -219

No files matched your search

+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
-66
View File
@@ -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
View File
@@ -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
View File
@@ -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 {