Feat: found a fast solution to hook state

This commit is contained in:
Jonathan Kelley committed 2021-02-06 22:19:56 -05:00
1 parent 62d4ad5878
commit 4d01436c3f
9 files changed
+759 -85

No files matched your search

+13 -1
View File
@@ -70,6 +70,10 @@ pub mod nodes;
pub mod validation;
pub mod virtual_dom;
pub mod builder {
pub use super::nodebuilder::*;
}
/// Re-export common types for ease of development use.
/// Essential when working with the html! macro
///
@@ -89,8 +93,16 @@ pub mod prelude {
pub type VirtualNode<'a> = VNode<'a>;
// Re-export from the macro crate
pub use dioxus_html_macro::html;
// pub use dodrio_derive::html;
pub use bumpalo;
// pub use dioxus_html_macro::html;
// Re-export the FC macro
pub use dioxus_core_macro::fc;
pub use dioxus_html_macro::html;
pub use crate as dioxus;
pub use crate::nodebuilder as builder;
}
+235 -64
View File
@@ -7,8 +7,8 @@ the DomTree trait is simply an abstraction over a lazy dom builder, much like th
This means we can accept DomTree anywhere as well as return it. All components therefore look like this:
```ignore
function Component(ctx: Context<()>) -> impl DomTree {
html! {<div> "hello world" </div>}
function Component(ctx: Context<()>) -> VNode {
ctx.view(html! {<div> "hello world" </div>})
}
```
It's not quite as sexy as statics, but there's only so much you can do. The goal is to get statics working with the FC macro,
@@ -18,7 +18,7 @@ into its own lib (IE, lazy loading wasm chunks by function (exciting stuff!))
```ignore
#[fc] // gets translated into a function.
static Component: FC = |ctx| {
html! {<div> "hello world" </div>}
ctx.view(html! {<div> "hello world" </div>})
}
```
@@ -50,70 +50,152 @@ A Context
use crate::nodes::VNode;
use crate::prelude::*;
use bumpalo::Bump;
use generational_arena::Arena;
use std::future::Future;
use generational_arena::{Arena, Index};
use std::{
any::TypeId,
cell::{RefCell, UnsafeCell},
future::Future,
sync::atomic::AtomicUsize,
};
/// An integrated virtual node system that progresses events and diffs UI trees.
/// Differences are converted into patches which a renderer can use to draw the UI.
pub struct VirtualDom {
pub struct VirtualDom<P: Properties> {
/// All mounted components are arena allocated to make additions, removals, and references easy to work with
/// A generational arean is used to re-use slots of deleted scopes without having to resize the underlying arena.
components: Arena<Scope>,
base_scope: Index,
/// Components generate lifecycle events
event_queue: Vec<LifecycleEvent>,
buffers: [Bump; 2],
selected_buf: u8,
root_props: P,
}
impl VirtualDom {
/// Implement VirtualDom with no props for components that initialize their state internal to the VDom rather than externally.
impl VirtualDom<()> {
/// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
///
/// This means that the root component must either consumes its own context, or statics are used to generate the page.
/// The root component can access things like routing in its context.
pub fn new(root: FC<()>) -> Self {
Self::new_with_props(root)
Self::new_with_props(root, ())
}
}
/// Implement the VirtualDom for any Properties
impl<P: Properties + 'static> VirtualDom<P> {
/// Start a new VirtualDom instance with a dependent props.
/// 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.
pub fn new_with_props<P: Properties>(root: FC<P>) -> Self {
pub fn new_with_props(root: FC<P>, root_props: P) -> Self {
// 1. Create the buffers
// 2. Create the component arena
// 3. Create the base scope (can never be removed)
// 4. Create the lifecycle queue
// 5. Create the event queue
let buffers = [Bump::new(), Bump::new()];
// Arena allocate all the components
// This should make it *really* easy to store references in events and such
let mut components = Arena::new();
// Create a reference to the component in the arena
let base_scope = components.insert(Scope::new(root));
// Create an event queue with a mount for the base scope
let event_queue = vec![];
Self {
components: Arena::new(),
event_queue: vec![],
buffers: [Bump::new(), Bump::new()],
components,
base_scope,
event_queue,
buffers,
root_props,
selected_buf: 0,
}
}
/// Pop an event off the even queue and process it
pub fn progress_event() {}
}
pub fn progress(&mut self) -> Result<(), ()> {
let LifecycleEvent { index, event_type } = self.event_queue.pop().ok_or(())?;
/// The internal lifecycle event system is managed by these
/// All events need to be confused before swapping doms over
pub enum LifecycleEvent {
Add {},
}
let scope = self.components.get(index).ok_or(())?;
/// Anything that takes a "bump" and returns VNodes is a "DomTree"
/// This is used as a "trait alias" for function return types to look less hair
pub trait DomTree {
fn render(self, b: &Bump) -> VNode;
}
match event_type {
// Component needs to be mounted to the virtual dom
LifecycleType::Mount {} => {
// todo! run the FC with the bump allocator
// Run it with its properties
}
/// Implement DomTree for the type returned by the html! macro.
/// This lets the caller of the static function evaluate the builder closure with its own bump.
/// It keeps components pretty and removes the need for the user to get too involved with allocation.
impl<F> DomTree for F
where
F: FnOnce(&Bump) -> VNode,
{
fn render(self, b: &Bump) -> VNode {
self(b)
// The parent for this component generated new props and the component needs update
LifecycleType::PropsChanged {} => {}
// Component was successfully mounted to the dom
LifecycleType::Mounted {} => {}
// Component was removed from the DOM
// Run any destructors and cleanup for the hooks and the dump the component
LifecycleType::Removed {} => {
let f = self.components.remove(index);
}
// Component was moved around in the DomTree
// Doesn't generate any event but interesting to keep track of
LifecycleType::Moved {} => {}
// Component was messaged via the internal subscription service
LifecycleType::Messaged => {}
}
Ok(())
}
/// Update the root props, causing a full event cycle
pub fn update_props(&mut self, new_props: P) {}
/// Run through every event in the event queue until the events are empty.
/// Function is asynchronous to allow for async components to finish their work.
pub async fn progess_completely() {}
/// Create a new context object for a given component and scope
fn new_context<T: Properties>(&self) -> Context<T> {
todo!()
}
/// Stop writing to the current buffer and start writing to the new one.
/// This should be done inbetween CallbackEvent handling, but not between lifecycle events.
pub fn swap_buffers(&mut self) {}
}
pub struct LifecycleEvent {
pub index: Index,
pub event_type: LifecycleType,
}
impl LifecycleEvent {
fn mount(index: Index) -> Self {
Self {
index,
event_type: LifecycleType::Mount,
}
}
}
/// The internal lifecycle event system is managed by these
pub enum LifecycleType {
Mount,
PropsChanged,
Mounted,
Removed,
Moved,
Messaged,
}
/// The `Component` trait refers to any struct or funciton that can be used as a component
@@ -152,37 +234,38 @@ impl Properties for () {
#[cfg(test)]
mod fc_test {
use super::*;
use crate::prelude::*;
// // Make sure this function builds properly.
// fn test_static_fn<'a, P: Properties, F: DomTree>(b: &'a Bump, r: &FC<P, F>) -> VNode<'a> {
// let p = P::new(); // new props
// let c = Context { props: p }; // new context with props
// let g = r(&c); // calling function with context
// g.render(&b) // rendering closure with bump allocator
// }
// Make sure this function builds properly.
fn test_static_fn<'a, P: Properties>(b: &'a Bump, r: FC<P>) -> VNode<'a> {
todo!()
// let p = P::new(); // new props
// let c = Context { props: &p }; // new context with props
// let g = r(&c); // calling function with context
// g
}
// fn test_component(ctx: &Context<()>) -> impl DomTree {
// // todo: helper should be part of html! macro
// html! { <div> </div> }
// }
fn test_component<'a>(ctx: &'a Context<()>) -> VNode<'a> {
// todo: helper should be part of html! macro
todo!()
// ctx.view(|bump| html! {bump, <div> </div> })
}
// fn test_component2(ctx: &Context<()>) -> impl DomTree {
// __domtree_helper(move |bump: &Bump| VNode::text("blah"))
// }
fn test_component2<'a>(ctx: &'a Context<()>) -> VNode<'a> {
ctx.view(|bump: &Bump| VNode::text("blah"))
}
// #[test]
// fn ensure_types_work() {
// // TODO: Get the whole casting thing to work properly.
// // For whatever reason, FC is not auto-implemented, depsite it being a static type
// let b = Bump::new();
#[test]
fn ensure_types_work() {
// TODO: Get the whole casting thing to work properly.
// For whatever reason, FC is not auto-implemented, depsite it being a static type
let b = Bump::new();
// let g: FC<_, _> = test_component;
// let nodes0 = test_static_fn(&b, &g);
// // Happiness! The VNodes are now allocated onto the bump vdom
// Happiness! The VNodes are now allocated onto the bump vdom
let nodes0 = test_static_fn(&b, test_component);
// let g: FC<_, _> = test_component2;
// let nodes1 = test_static_fn(&b, &g);
// }
let nodes1 = test_static_fn(&b, test_component2);
}
}
/// The Scope that wraps a functional component
@@ -190,19 +273,30 @@ mod fc_test {
/// The actualy contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently.
pub struct Scope {
hook_idx: i32,
hooks: Vec<()>,
hooks: Vec<OLDHookState>,
props_type: TypeId,
}
impl Scope {
fn new<T>() -> Self {
// create a new scope from a function
fn new<T: 'static>(f: FC<T>) -> Self {
// Capture the props type
let props_type = TypeId::of::<T>();
// Obscure the function
Self {
hook_idx: 0,
hooks: vec![],
props_type,
}
}
/// Create a new context and run the component with references from the Virtual Dom
/// This function downcasts the function pointer based on the stored props_type
fn run() {}
}
pub struct HookState {}
pub struct OLDHookState {}
/// Components in Dioxus use the "Context" object to interact with their lifecycle.
/// This lets components schedule updates, integrate hooks, and expose their context via the context api.
@@ -224,13 +318,16 @@ pub struct HookState {}
/// ```
// todo: force lifetime of source into T as a valid lifetime too
// it's definitely possible, just needs some more messing around
pub struct Context<'source, T> {
pub struct Context<'src, T> {
/// Direct access to the properties used to create this component.
pub props: &'source T,
pub props: T,
pub idx: AtomicUsize,
pub arena: &'src typed_arena::Arena<Hook>,
pub hooks: RefCell<Vec<*mut Hook>>,
pub _p: std::marker::PhantomData<&'src ()>,
}
impl<'a, T> Context<'a, T> {
// impl<'a, T> Context<'a, T> {
/// Access the children elements passed into the component
pub fn children(&self) -> Vec<VNode> {
todo!("Children API not yet implemented for component Context")
@@ -271,4 +368,78 @@ impl<'a, T> Context<'a, T> {
) -> VNode<'a> {
todo!()
}
/// use_hook provides a way to store data between renders for functional components.
pub fn use_hook<'comp, InternalHookState: 'static, Output: 'static>(
&'comp self,
// The closure that builds the hook state
initializer: impl FnOnce() -> InternalHookState,
// The closure that takes the hookstate and returns some value
runner: impl for<'b> FnOnce(&'comp mut InternalHookState) -> &'comp Output,
// The closure that cleans up whatever mess is left when the component gets torn down
// TODO: add this to the "clean up" group for when the component is dropped
tear_down: impl FnOnce(InternalHookState),
) -> &'comp Output {
let raw_hook = {
let idx = self.idx.load(std::sync::atomic::Ordering::Relaxed);
// Mutate hook list if necessary
let mut hooks = self.hooks.borrow_mut();
// Initialize the hook by allocating it in the typed arena.
// We get a reference from the arena which is owned by the component scope
// This is valid because "Context" is only valid while the scope is borrowed
if idx >= hooks.len() {
let new_state = initializer();
let boxed_state: Box<dyn std::any::Any> = Box::new(new_state);
let hook = self.arena.alloc(Hook::new(boxed_state));
// Push the raw pointer instead of the &mut
// A "poor man's OwningRef"
hooks.push(hook);
}
self.idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
*hooks.get(idx).unwrap()
};
/*
** UNSAFETY ALERT **
Here, we dereference a raw pointer. Normally, we aren't guaranteed that this is okay.
However, typed-arena gives a mutable reference to the stored data which is stable for any inserts
into the arena. During the first call of the function, we need to add the mutable reference given to use by
the arena into our list of hooks. The arena provides stability of the &mut references and is only deallocated
when the component itself is deallocated.
This is okay because:
- The lifetime of the component arena is tied to the lifetime of these raw hooks
- Usage of the raw hooks is tied behind the Vec refcell
- Output is static, meaning it can't take a reference to the data
- We don't expose the raw hook pointer outside of the scope of use_hook
*/
let borrowed_hook: &'comp mut _ = unsafe { raw_hook.as_mut().unwrap() };
let internal_state = borrowed_hook
.state
.downcast_mut::<InternalHookState>()
.unwrap();
runner(internal_state)
}
}
pub struct Hook {
state: Box<dyn std::any::Any>,
}
impl Hook {
fn new(state: Box<dyn std::any::Any>) -> Self {
Self { state }
}
}
/// A CallbackEvent wraps any event returned from the renderer's event system.
pub struct CallbackEvent {}
pub struct EventListener {}