concept: integrate signals

This commit is contained in:
Jonathan Kelley 2021-06-30 14:08:12 -04:00
parent 7665f2c6cf
commit 93900aac44
10 changed files with 203 additions and 91 deletions

View file

@ -56,7 +56,7 @@ members = [
"packages/core",
"packages/html-namespace",
"packages/web",
"packages/webview"
# "packages/webview"
# "packages/cli",
# "packages/atoms",
# "packages/ssr",

View file

@ -25,15 +25,17 @@ If you know React, then you already know Dioxus.
### **Things you'll love ❤️:**
- Ergonomic design
- Minimal boilerplate
- **Ergonomic** design
- **Minimal** boilerplate
- Familiar design and semantics
- Simple build, test, and deploy
- Compile-time correct templating
- Support for html! and rsx! templating
- **Compile-time correct** templating
- Support for **fine-grained reactivity**
- Support for **html!** and **rsx!** templating
- SSR, WASM, desktop, and mobile support
- Powerful and simple integrated state management
- Rust! (enums, static types, modules, efficiency)
- Support for **asynchronous** batched rendering
- Powerful and simple **integrated state management**
- **Rust!** (enums, static types, modules, efficiency)
## Get Started with...
@ -96,27 +98,28 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an
### Phase 1: The Basics
| Feature | Dioxus | React | Notes for Dioxus |
| ---------------------- | ------ | ----- | ------------------------------------------------ |
| Conditional Rendering | ✅ | ✅ | if/then to hide/show component |
| Map, Iterator | ✅ | ✅ | map/filter/reduce rsx! |
| Keyed Components | ✅ | ✅ | advanced diffing with keys |
| Web | ✅ | ✅ | renderer for web browser |
| Desktop (webview) | ✅ | ✅ | renderer for desktop |
| Context | ✅ | ✅ | share state through the tree |
| Hook | ✅ | ✅ | memory cells in components |
| SSR | ✅ | ✅ | render directly to string |
| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) |
| Component Children | ✅ | ✅ | cx.children() as a list of nodes |
| Null components | ✅ | ✅ | allow returning no components |
| No-div components | ✅ | ✅ | components that render components |
| Fragments | ✅ | ✅ | rsx! can return multiple elements without a root |
| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax |
| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs |
| Suspense | 🛠 | 🛠 | schedule future render from future/promise |
| 1st class global state | 🛠 | ✅ | redux/recoil/mobx on top of context |
| CSS/Inline Styles | 🛠 | ✅ | syntax for inline styles/attribute groups[2] |
| NodeRef | 🛠 | ✅ | gain direct access to nodes [1] |
| Feature | Dioxus | React | Notes for Dioxus |
| ----------------------- | ------ | ----- | ------------------------------------------------ |
| Conditional Rendering | ✅ | ✅ | if/then to hide/show component |
| Map, Iterator | ✅ | ✅ | map/filter/reduce rsx! |
| Keyed Components | ✅ | ✅ | advanced diffing with keys |
| Web | ✅ | ✅ | renderer for web browser |
| Desktop (webview) | ✅ | ✅ | renderer for desktop |
| Context | ✅ | ✅ | share state through the tree |
| Hook | ✅ | ✅ | memory cells in components |
| SSR | ✅ | ✅ | render directly to string |
| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) |
| Component Children | ✅ | ✅ | cx.children() as a list of nodes |
| Null components | ✅ | ✅ | allow returning no components |
| No-div components | ✅ | ✅ | components that render components |
| Fragments | ✅ | ✅ | rsx! can return multiple elements without a root |
| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax |
| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs |
| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates |
| Suspense | 🛠 | 🛠 | schedule future render from future/promise |
| 1st class global state | 🛠 | ✅ | redux/recoil/mobx on top of context |
| CSS/Inline Styles | 🛠 | ✅ | syntax for inline styles/attribute groups[2] |
| NodeRef | 🛠 | ✅ | gain direct access to nodes [1] |
- [1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API.
- [2] Would like to solve this in a more general way. Something like attribute groups that's not styling-specific.

View file

@ -0,0 +1,7 @@
# Signals: Skipping the Diff
In most cases, the traditional VirtualDOM diffing pattern is plenty fast. Dioxus will compare trees of VNodes, find the differences, and then update the Renderer's DOM with the updates. However, this can generate a lot of overhead for certain types of components. In apps where reducing visual latency is a top priority, you can opt into the `Signals` api to entirely disable diffing of hot-path components. Dioxus will then automatically construct a state machine for your component, making updates nearly instant.
What does this look like?
## How does it work?

View file

@ -22,9 +22,6 @@ thiserror = "1"
# faster hashmaps
fxhash = "0.2.1"
# even *faster* hashmaps for index-based types
nohash-hasher = "0.2.0"
# Used in diffing
longest-increasing-subsequence = "0.1.0"
@ -39,6 +36,8 @@ smallvec = "1.6.1"
# Backs scopes and unique keys
slotmap = "1.0.3"
# backs the fiber system for suspended components
# todo: would like to use something smaller or just roll our own futures manually
futures = "0.3.15"

View file

@ -42,6 +42,7 @@ pros:
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........................
@ -54,4 +55,10 @@ cons:
## idea: leak raw nodes and then reclaim them on drop
## idea: bind
# Fiber/Concurrency
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.
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

View file

@ -377,7 +377,6 @@ pub struct Scope {
pub event_channel: Rc<dyn Fn() + 'static>,
// pub event_queue: EventQueue,
pub caller: Weak<OpaqueComponent>,
pub hookidx: Cell<usize>,
@ -422,7 +421,6 @@ impl Scope {
// Therefore, their lifetimes are connected exclusively to the virtual dom
pub fn new<'creator_node>(
caller: Weak<OpaqueComponent>,
// caller: Weak<OpaqueComponent<'creator_node>>,
arena_idx: ScopeIdx,
parent: Option<ScopeIdx>,
height: u32,

View file

@ -24,7 +24,6 @@ once_cell = "1.7.2"
atoms = { path="../atoms" }
async-channel = "1.6.1"
nohash-hasher = "0.2.0"
anyhow = "1.0.41"
slotmap = "1.0.3"

View file

@ -0,0 +1,125 @@
use std::{collections::HashMap, rc::Rc};
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_web::WebsysRenderer;
static APP_STYLE: &'static str = include_str!("./todomvc/style.css");
fn main() {
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App));
}
#[derive(PartialEq)]
pub enum FilterState {
All,
Active,
Completed,
}
#[derive(Debug, PartialEq, Clone)]
pub struct TodoItem {
pub id: uuid::Uuid,
pub checked: bool,
pub contents: String,
}
pub fn App(cx: Context<()>) -> VNode {
let (draft, set_draft) = use_state(&cx, || "".to_string());
let (todos, set_todos) = use_state(&cx, || HashMap::<uuid::Uuid, Rc<TodoItem>>::new());
let (filter, set_filter) = use_state(&cx, || FilterState::All);
let filtered_todos = todos.iter().filter(move |(id, item)| match filter {
FilterState::All => true,
FilterState::Active => !item.checked,
FilterState::Completed => item.checked,
});
let items_left = filtered_todos.clone().count();
let item_text = match items_left {
1 => "item",
_ => "items",
};
cx.render(rsx! {
div { id: "app"
div {
header { class: "header"
h1 {"todos"}
input {
class: "new-todo"
placeholder: "What needs to be done?"
value: "{draft}"
oninput: move |evt| set_draft(evt.value())
}
}
{filtered_todos.map(|(id, item)| {
rsx!(TodoEntry {
key: "{id}",
item: item.clone()
})
})}
// filter toggle (show only if the list isn't empty)
{(!todos.is_empty()).then(|| rsx!(
footer {
span {
strong {"{items_left}"}
span {"{item_text} left"}
}
ul {
class: "filters"
li { class: "All", a { href: "", onclick: move |_| set_filter(FilterState::All), "All" }}
li { class: "Active", a { href: "active", onclick: move |_| set_filter(FilterState::Active), "Active" }}
li { class: "Completed", a { href: "completed", onclick: move |_| set_filter(FilterState::Completed), "Completed" }}
}
}
))}
}
// footer
footer {
class: "info"
p {"Double-click to edit a todo"}
p {
"Created by "
a { "jkelleyrtp", href: "http://github.com/jkelleyrtp/" }
}
p {
"Part of "
a { "TodoMVC", href: "http://todomvc.com" }
}
}
}
})
}
#[derive(PartialEq, Props)]
pub struct TodoEntryProps {
item: Rc<TodoItem>,
}
pub fn TodoEntry(cx: Context<TodoEntryProps>) -> VNode {
let (is_editing, set_is_editing) = use_state(&cx, || false);
let contents = "";
let todo = TodoItem {
checked: false,
contents: "asd".to_string(),
id: uuid::Uuid::new_v4(),
};
cx.render(rsx! (
li {
"{todo.id}"
input {
class: "toggle"
type: "checkbox"
"{todo.checked}"
}
{is_editing.then(|| rsx!{
input {
value: "{contents}"
}
})}
}
))
}

View file

@ -8,6 +8,7 @@
//!
//! Here, we show to use Dioxus' Recoil state management solution to simplify app logic
#![allow(non_snake_case)]
use dioxus_core as dioxus;
use dioxus_web::dioxus::prelude::*;
use std::collections::HashMap;
@ -141,39 +142,27 @@ pub fn FilterToggles(cx: Context<()>) -> VNode {
let reducer = TodoManager(use_recoil_api(cx));
let items_left = use_read(cx, &TODOS_LEFT);
let toggles = [
("All", "", FilterState::All),
("Active", "active", FilterState::Active),
("Completed", "completed", FilterState::Completed),
]
.iter()
.map(|(name, path, filter)| {
rsx!(
li { class: "{name}"
a {
href: "{path}",
onclick: move |_| reducer.set_filter(&filter),
"{name}",
}
}
)
});
let item_text = match items_left {
1 => "item",
_ => "items",
};
let toggles = rsx! {
ul {
class: "filters"
li { class: "All", a { href: "", onclick: move |_| reducer.set_filter(&FilterState::All), "All" }}
li { class: "Active", a { href: "active", onclick: move |_| reducer.set_filter(&FilterState::Active), "Active" }}
li { class: "Completed", a { href: "completed", onclick: move |_| reducer.set_filter(&FilterState::Completed), "Completed" }}
}
};
rsx! { in cx,
footer {
span {
strong {"{items_left}"}
span { "{item_text} left" }
}
ul {
class: "filters"
{toggles}
}
{toggles}
}
}
}

View file

@ -6,7 +6,6 @@ use dioxus_core::{
virtual_dom::RealDomNode,
};
use fxhash::FxHashMap;
use nohash_hasher::IntMap;
use slotmap::{DefaultKey, Key, KeyData};
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{
@ -20,25 +19,19 @@ pub struct WebsysDom {
root: Element,
event_receiver: async_channel::Receiver<EventTrigger>,
trigger: Arc<dyn Fn(EventTrigger)>,
// every callback gets a monotomically increasing callback ID
callback_id: usize,
// map of listener types to number of those listeners
listeners: FxHashMap<String, (usize, Closure<dyn FnMut(&Event)>)>,
// Map of callback_id to component index and listener id
callback_map: FxHashMap<usize, (usize, usize)>,
// This is roughly a delegater
// TODO: check how infero delegates its events - some are more performant
listeners: FxHashMap<&'static str, (usize, Closure<dyn FnMut(&Event)>)>,
// We need to make sure to add comments between text nodes
// We ensure that the text siblings are patched by preventing the browser from merging
// neighboring text nodes. Originally inspired by some of React's work from 2016.
// -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
// -> https://github.com/facebook/react/pull/5753
//
// `ptns` = Percy text node separator
// TODO
last_node_was_text: bool,
}
impl WebsysDom {
@ -48,7 +41,7 @@ impl WebsysDom {
.document()
.expect("must have access to the Document");
let (sender, mut receiver) = async_channel::unbounded::<EventTrigger>();
let (sender, receiver) = async_channel::unbounded::<EventTrigger>();
let sender_callback = Arc::new(move |ev| {
let c = sender.clone();
@ -57,19 +50,13 @@ impl WebsysDom {
});
});
let mut nodes = slotmap::SlotMap::new();
// HashMap::with_capacity_and_hasher(1000, nohash_hasher::BuildNoHashHasher::default());
// let mut nodes =
// HashMap::with_capacity_and_hasher(1000, nohash_hasher::BuildNoHashHasher::default());
let mut nodes = slotmap::SlotMap::with_capacity(1000);
let root_id = nodes.insert(root.clone().dyn_into::<Node>().unwrap());
Self {
stack: Stack::with_capacity(10),
nodes,
callback_id: 0,
listeners: FxHashMap::default(),
callback_map: FxHashMap::default(),
document,
event_receiver: receiver,
trigger: sender_callback,
@ -208,7 +195,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
fn new_event_listener(
&mut self,
event: &str,
event: &'static str,
scope: dioxus_core::prelude::ScopeIdx,
el_id: usize,
real_id: RealDomNode,
@ -231,10 +218,10 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
.dyn_ref::<Element>()
.expect(&format!("not an element: {:?}", el));
let (gi_id, gi_gen) = (&scope).into_raw_parts();
let gi_id = scope.data().as_ffi();
el.set_attribute(
&format!("dioxus-event-{}", event),
&format!("{}.{}.{}.{}", gi_id, gi_gen, el_id, real_id.0),
&format!("{}.{}.{}", gi_id, el_id, real_id.0),
)
.unwrap();
@ -565,13 +552,9 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
.get_attribute(&format!("dioxus-event-{}", typ))
.context("")?;
let mut fields = val.splitn(4, ".");
let mut fields = val.splitn(3, ".");
let gi_id = fields
.next()
.and_then(|f| f.parse::<usize>().ok())
.context("")?;
let gi_gen = fields
.next()
.and_then(|f| f.parse::<u64>().ok())
.context("")?;
@ -585,17 +568,19 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
.context("")?;
// Call the trigger
log::debug!(
"decoded gi_id: {}, gi_gen: {}, li_idx: {}",
gi_id,
gi_gen,
el_id
);
log::debug!("decoded gi_id: {}, li_idx: {}", gi_id, el_id);
let triggered_scope = ScopeIdx::from_raw_parts(gi_id, gi_gen);
let triggered_scope: ScopeIdx = KeyData::from_ffi(gi_id).into();
Ok(EventTrigger::new(
virtual_event_from_websys_event(event),
triggered_scope,
real_id,
))
}
struct ListenerMap {}
impl ListenerMap {
fn get(&self, event: &'static str) -> bool {
false
}
}