chore: clean up scheduler code
This commit is contained in:
1 parent
c096057dd3
commit
f5bc137f01
17 files changed
+439
-294
No files matched your search
@@ -1,12 +1,10 @@
|
||||
use crate::any_props::VComponentProps;
|
||||
use crate::arena::ElementPath;
|
||||
use crate::component::Component;
|
||||
use crate::diff::DirtyScope;
|
||||
use crate::factory::RenderReturn;
|
||||
use crate::innerlude::{Renderer, Scheduler, SchedulerMsg};
|
||||
use crate::innerlude::{Mutations, Scheduler, SchedulerMsg};
|
||||
use crate::mutations::Mutation;
|
||||
use crate::nodes::{Template, TemplateId};
|
||||
|
||||
use crate::{
|
||||
arena::ElementId,
|
||||
scopes::{ScopeId, ScopeState},
|
||||
@@ -14,10 +12,101 @@ use crate::{
|
||||
use crate::{scheduler, Element, Scope};
|
||||
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use futures_util::Future;
|
||||
use scheduler::{SuspenseBoundary, SuspenseContext, SuspenseId};
|
||||
use scheduler::{SuspenseBoundary, SuspenseId};
|
||||
use slab::Slab;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
/// A virtual node system that progresses user events and diffs UI trees.
|
||||
///
|
||||
/// ## Guide
|
||||
///
|
||||
/// Components are defined as simple functions that take [`Scope`] and return an [`Element`].
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// #[derive(Props, PartialEq)]
|
||||
/// struct AppProps {
|
||||
/// title: String
|
||||
/// }
|
||||
///
|
||||
/// fn App(cx: Scope<AppProps>) -> Element {
|
||||
/// cx.render(rsx!(
|
||||
/// div {"hello, {cx.props.title}"}
|
||||
/// ))
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Components may be composed to make complex apps.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn App(cx: Scope<AppProps>) -> Element {
|
||||
/// cx.render(rsx!(
|
||||
/// NavBar { routes: ROUTES }
|
||||
/// Title { "{cx.props.title}" }
|
||||
/// Footer {}
|
||||
/// ))
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// To start an app, create a [`VirtualDom`] and call [`VirtualDom::rebuild`] to get the list of edits required to
|
||||
/// draw the UI.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let mut vdom = VirtualDom::new(App);
|
||||
/// let edits = vdom.rebuild();
|
||||
/// ```
|
||||
///
|
||||
/// To inject UserEvents into the VirtualDom, call [`VirtualDom::get_scheduler_channel`] to get access to the scheduler.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let channel = vdom.get_scheduler_channel();
|
||||
/// channel.send_unbounded(SchedulerMsg::UserEvent(UserEvent {
|
||||
/// // ...
|
||||
/// }))
|
||||
/// ```
|
||||
///
|
||||
/// While waiting for UserEvents to occur, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// vdom.wait_for_work().await;
|
||||
/// ```
|
||||
///
|
||||
/// Once work is ready, call [`VirtualDom::work_with_deadline`] to compute the differences between the previous and
|
||||
/// current UI trees. This will return a [`Mutations`] object that contains Edits, Effects, and NodeRefs that need to be
|
||||
/// handled by the renderer.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let mutations = vdom.work_with_deadline(|| false);
|
||||
/// for edit in mutations {
|
||||
/// apply(edit);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Building an event loop around Dioxus:
|
||||
///
|
||||
/// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn App(cx: Scope) -> Element {
|
||||
/// cx.render(rsx!{
|
||||
/// div { "Hello World" }
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// async fn main() {
|
||||
/// let mut dom = VirtualDom::new(App);
|
||||
///
|
||||
/// let mut inital_edits = dom.rebuild();
|
||||
/// apply_edits(inital_edits);
|
||||
///
|
||||
/// loop {
|
||||
/// dom.wait_for_work().await;
|
||||
/// let frame_timeout = TimeoutFuture::new(Duration::from_millis(16));
|
||||
/// let deadline = || (&mut frame_timeout).now_or_never();
|
||||
/// let edits = dom.run_with_deadline(deadline).await;
|
||||
/// apply_edits(edits);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub struct VirtualDom {
|
||||
pub(crate) templates: HashMap<TemplateId, Template<'static>>,
|
||||
pub(crate) elements: Slab<ElementPath>,
|
||||
@@ -28,59 +117,153 @@ pub struct VirtualDom {
|
||||
|
||||
// While diffing we need some sort of way of breaking off a stream of suspended mutations.
|
||||
pub(crate) scope_stack: Vec<ScopeId>,
|
||||
pub(crate) waiting_on: Vec<SuspenseId>,
|
||||
pub(crate) collected_leaves: Vec<SuspenseId>,
|
||||
|
||||
// Whenever a suspense tree is finished, we push its boundary onto this stack.
|
||||
// When "render_with_deadline" is called, we pop the stack and return the mutations
|
||||
pub(crate) finished_fibers: Vec<ScopeId>,
|
||||
|
||||
pub(crate) rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
|
||||
}
|
||||
|
||||
impl VirtualDom {
|
||||
/// Create a new VirtualDom with a component that does not have special props.
|
||||
///
|
||||
/// # Description
|
||||
///
|
||||
/// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
|
||||
///
|
||||
/// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
|
||||
/// to toss out the entire tree.
|
||||
///
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, ignore
|
||||
/// fn Example(cx: Scope) -> Element {
|
||||
/// cx.render(rsx!( div { "hello world" } ))
|
||||
/// }
|
||||
///
|
||||
/// let dom = VirtualDom::new(Example);
|
||||
/// ```
|
||||
///
|
||||
/// Note: the VirtualDom is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it.
|
||||
pub fn new(app: fn(Scope) -> Element) -> Self {
|
||||
let scheduler = Scheduler::new();
|
||||
Self::new_with_props(app, ())
|
||||
}
|
||||
|
||||
let mut res = Self {
|
||||
/// Create a new VirtualDom with the given properties for the root component.
|
||||
///
|
||||
/// # Description
|
||||
///
|
||||
/// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
|
||||
///
|
||||
/// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
|
||||
/// to toss out the entire tree.
|
||||
///
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, ignore
|
||||
/// #[derive(PartialEq, Props)]
|
||||
/// struct SomeProps {
|
||||
/// name: &'static str
|
||||
/// }
|
||||
///
|
||||
/// fn Example(cx: Scope<SomeProps>) -> Element {
|
||||
/// cx.render(rsx!{ div{ "hello {cx.props.name}" } })
|
||||
/// }
|
||||
///
|
||||
/// let dom = VirtualDom::new(Example);
|
||||
/// ```
|
||||
///
|
||||
/// Note: the VirtualDom is not progressed on creation. You must either "run_with_deadline" or use "rebuild" to progress it.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
|
||||
/// let mutations = dom.rebuild();
|
||||
/// ```
|
||||
pub fn new_with_props<P>(root: fn(Scope<P>) -> Element, root_props: P) -> Self
|
||||
where
|
||||
P: 'static,
|
||||
{
|
||||
let channel = futures_channel::mpsc::unbounded();
|
||||
Self::new_with_props_and_scheduler(root, root_props, channel)
|
||||
}
|
||||
|
||||
/// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the scheduler
|
||||
///
|
||||
/// This is useful when the VirtualDom must be driven from outside a thread and it doesn't make sense to wait for the
|
||||
/// VirtualDom to be created just to retrieve its channel receiver.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let channel = futures_channel::mpsc::unbounded();
|
||||
/// let dom = VirtualDom::new_with_scheduler(Example, (), channel);
|
||||
/// ```
|
||||
pub fn new_with_props_and_scheduler<P: 'static>(
|
||||
root: fn(Scope<P>) -> Element,
|
||||
root_props: P,
|
||||
(tx, rx): (
|
||||
UnboundedSender<SchedulerMsg>,
|
||||
UnboundedReceiver<SchedulerMsg>,
|
||||
),
|
||||
) -> Self {
|
||||
let mut dom = Self {
|
||||
rx,
|
||||
scheduler: Scheduler::new(tx),
|
||||
templates: Default::default(),
|
||||
scopes: Slab::default(),
|
||||
elements: Default::default(),
|
||||
scope_stack: Vec::new(),
|
||||
element_stack: vec![ElementId(0)],
|
||||
dirty_scopes: BTreeSet::new(),
|
||||
waiting_on: Vec::new(),
|
||||
scheduler,
|
||||
collected_leaves: Vec::new(),
|
||||
finished_fibers: Vec::new(),
|
||||
};
|
||||
|
||||
let props = Box::into_raw(Box::new(VComponentProps::new_empty(app)));
|
||||
let props: *mut VComponentProps<(), ()> = unsafe { std::mem::transmute(props) };
|
||||
dom.new_scope(Box::into_raw(Box::new(VComponentProps::new(
|
||||
root,
|
||||
|_, _| unreachable!(),
|
||||
root_props,
|
||||
))))
|
||||
// The root component is always a suspense boundary for any async children
|
||||
// This could be unexpected, so we might rethink this behavior
|
||||
.provide_context(SuspenseBoundary::new(ScopeId(0)));
|
||||
|
||||
let root = res.new_scope(props);
|
||||
|
||||
// the root component is always a suspense boundary for any async children
|
||||
res.scopes[root.0].provide_context(SuspenseBoundary::new(root));
|
||||
assert_eq!(root, ScopeId(0));
|
||||
|
||||
res
|
||||
dom
|
||||
}
|
||||
|
||||
/// Render the virtualdom, without processing any suspense.
|
||||
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch.
|
||||
///
|
||||
/// This does register futures with wakers, but does not process any of them.
|
||||
pub fn rebuild<'a>(&'a mut self) -> Renderer<'a> {
|
||||
let mut mutations = Renderer::new(0);
|
||||
let root_node: &RenderReturn = self.run_scope(ScopeId(0));
|
||||
let root_node: &RenderReturn = unsafe { std::mem::transmute(root_node) };
|
||||
/// The diff machine expects the RealDom's stack to be the root of the application.
|
||||
///
|
||||
/// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the
|
||||
/// root component will be ran once and then diffed. All updates will flow out as mutations.
|
||||
///
|
||||
/// All state stored in components will be completely wiped away.
|
||||
///
|
||||
/// Any templates previously registered will remain.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, ignore
|
||||
/// static App: Component = |cx| cx.render(rsx!{ "hello world" });
|
||||
///
|
||||
/// let mut dom = VirtualDom::new();
|
||||
/// let edits = dom.rebuild();
|
||||
///
|
||||
/// apply_edits(edits);
|
||||
/// ```
|
||||
pub fn rebuild<'a>(&'a mut self) -> Mutations<'a> {
|
||||
let mut mutations = Mutations::new(0);
|
||||
|
||||
let mut created = 0;
|
||||
let root_node = unsafe { self.run_scope_extend(ScopeId(0)) };
|
||||
match root_node {
|
||||
RenderReturn::Sync(Some(node)) => {
|
||||
self.scope_stack.push(ScopeId(0));
|
||||
created = self.create(&mut mutations, node);
|
||||
self.scope_stack.pop();
|
||||
}
|
||||
RenderReturn::Sync(None) => {
|
||||
//
|
||||
let m = self.create_scope(ScopeId(0), &mut mutations, node);
|
||||
mutations.push(Mutation::AppendChildren { m });
|
||||
}
|
||||
RenderReturn::Sync(None) => {}
|
||||
RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"),
|
||||
}
|
||||
|
||||
mutations.push(Mutation::AppendChildren { m: created });
|
||||
|
||||
mutations
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user