소스 검색

wip: enable more diffing

Jonathan Kelley 4 년 전
부모
커밋
e8f29a8

+ 1 - 1
Cargo.toml

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

+ 136 - 0
examples/assets/calculator.css

@@ -0,0 +1,136 @@
+html {
+  box-sizing: border-box;
+}
+*, *:before, *:after {
+  box-sizing: inherit;
+}
+
+body {
+  margin: 0;
+  font: 100 14px 'Roboto';
+}
+
+button {
+  display: block;
+  background: none;
+  border: none;
+  padding: 0;
+  font-family: inherit;
+  user-select: none;
+  cursor: pointer;
+  outline: none;
+  
+  -webkit-tap-highlight-color: rgba(0,0,0,0);
+}
+
+button:active {
+  box-shadow: inset 0px 0px 80px 0px rgba(0,0,0,0.25);
+}
+
+#wrapper {
+  height: 100vh;
+  
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+#app {
+  width: 320px;
+  height: 520px;
+  position: relative;
+}
+
+.calculator {
+  width: 100%;
+  height: 100%;
+  background: black;
+  
+  display: flex;
+  flex-direction: column;
+}
+
+#wrapper .calculator {
+  box-shadow: 0px 0px 20px 0px #aaa;
+}
+
+.calculator-display {
+  color: white;
+  background: #1c191c;
+  line-height: 130px;
+  font-size: 6em;
+  
+  flex: 1;
+}
+
+.auto-scaling-text {
+  display: inline-block;
+}
+
+.calculator-display .auto-scaling-text {
+  padding: 0 30px;
+  position: absolute;
+  right: 0;
+  transform-origin: right;
+}
+
+.calculator-keypad {
+  height: 400px;
+  
+  display: flex;
+}
+
+.calculator .input-keys {
+  width: 240px;
+}
+
+.calculator .function-keys {
+  display: flex;
+}
+
+.calculator .digit-keys {
+  background: #e0e0e7;
+  
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap-reverse;
+}
+
+.calculator-key {
+  width: 80px;
+  height: 80px;
+  border-top: 1px solid #777;
+  border-right: 1px solid #666;  
+  text-align: center;
+  line-height: 80px;
+}
+.calculator .function-keys .calculator-key {
+  font-size: 2em;
+}
+.calculator .function-keys .key-multiply {
+  line-height: 50px;
+}
+.calculator .digit-keys .calculator-key {
+  font-size: 2.25em;
+}
+.calculator .digit-keys .key-0 {
+  width: 160px;
+  text-align: left;
+  padding-left: 32px;
+}
+.calculator .digit-keys .key-dot {
+  padding-top: 1em;
+  font-size: 0.75em;
+}
+.calculator .operator-keys .calculator-key {
+  color: white;
+  border-right: 0;
+  font-size: 3em;
+}
+
+.calculator .function-keys {
+  background: linear-gradient(to bottom, rgba(202,202,204,1) 0%, rgba(196,194,204,1) 100%);
+}
+.calculator .operator-keys {
+  background:  linear-gradient(to bottom, rgba(252,156,23,1) 0%, rgba(247,126,27,1) 100%);
+}

+ 9 - 6
examples/calculator.rs

@@ -98,16 +98,16 @@ static App: FC<()> = |cx| {
                     CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" }
                     CalculatorKey { name: "key-dot", onclick: move |_|  input_dot(), "●" }
 
-                    {(1..9).map(|k| rsx!{
+                    {(1..9).map(move |k| rsx!{
                         CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_|  input_digit(k), "{k}" }
                     })}
                 }
                 div { class: "operator-keys"
-                    CalculatorKey { name:"key-divide", onclick: move |_| operator.set(Some(Operator::Div)) "÷" }
-                    CalculatorKey { name:"key-multiply", onclick: move |_| operator.set(Some(Operator::Mul)) "×" }
-                    CalculatorKey { name:"key-subtract", onclick: move |_| operator.set(Some(Operator::Sub)) "−" }
-                    CalculatorKey { name:"key-add", onclick: move |_| operator.set(Some(Operator::Add)) "+" }
-                    CalculatorKey { name:"key-equals", onclick: move |_| perform_operation() "=" }
+                    CalculatorKey { name: "key-divide", onclick: move |_| operator.set(Some(Operator::Div)) "÷" }
+                    CalculatorKey { name: "key-multiply", onclick: move |_| operator.set(Some(Operator::Mul)) "×" }
+                    CalculatorKey { name: "key-subtract", onclick: move |_| operator.set(Some(Operator::Sub)) "−" }
+                    CalculatorKey { name: "key-add", onclick: move |_| operator.set(Some(Operator::Add)) "+" }
+                    CalculatorKey { name: "key-equals", onclick: move |_| perform_operation() "=" }
                 }
             }
         }
@@ -116,7 +116,10 @@ static App: FC<()> = |cx| {
 
 #[derive(Props)]
 struct CalculatorKeyProps<'a> {
+    /// Name!
     name: &'static str,
+
+    /// Click!
     onclick: &'a dyn Fn(MouseEvent),
 }
 

+ 60 - 0
examples/compose.rs

@@ -0,0 +1,60 @@
+fn main() {}
+
+struct Title(String);
+
+struct Position([f32; 3]);
+
+struct Velocity([f32; 3]);
+
+type Batch<T> = fn(&mut T) -> ();
+
+static Atom: Batch<(Title, Position, Velocity)> = |_| {};
+
+enum VNode<'a> {
+    El(El<'a>),
+    Text(&'a str),
+    Fragment(&'a [VNode<'a>]),
+}
+struct El<'a> {
+    name: &'static str,
+    key: Option<&'a str>,
+    attrs: &'a [(&'static str, AttrType<'a>)],
+    children: &'a [El<'a>],
+}
+enum AttrType<'a> {
+    Numeric(usize),
+    Text(&'a str),
+}
+
+fn example() {
+    use AttrType::Numeric;
+    let el = El {
+        name: "div",
+        attrs: &[("type", Numeric(10)), ("type", Numeric(10))],
+        key: None,
+        children: &[],
+    };
+}
+
+use dioxus::prelude::bumpalo::Bump;
+trait IntoVnode {
+    fn into_vnode<'a>(self, b: &'a Bump) -> VNode<'a>;
+}
+
+impl<'a> IntoIterator for VNode<'a> {
+    type Item = VNode<'a>;
+    type IntoIter = std::iter::Once<VNode<'a>>;
+
+    #[inline]
+    fn into_iter(self) -> Self::IntoIter {
+        std::iter::once(self)
+    }
+}
+
+fn take_iterable<F: IntoVnode>(f: impl IntoIterator<Item = F>) {
+    let iter = f.into_iter();
+    let b = Bump::new();
+    for f in iter {
+        let v = f.into_vnode(&b);
+    }
+}

+ 16 - 0
examples/doggo.rs

@@ -0,0 +1,16 @@
+use dioxus::prelude::*;
+fn main() {}
+
+static Example: FC<()> = |cx| {
+    let (g, set_g) = use_state(&cx, || 0);
+    let v = (0..10).map(move |f| {
+        rsx!(li {
+            onclick: move |_| set_g(10)
+        })
+    });
+    cx.render(rsx! {
+        div {
+            {v}
+        }
+    })
+};

+ 42 - 27
examples/model.rs

@@ -4,51 +4,54 @@
 //! Some components benefit through the use of "Models". Models are a single block of encapsulated state that allow mutative
 //! methods to be performed on them. Dioxus exposes the ability to use the model pattern through the "use_model" hook.
 //!
+//! Models are commonly used in the "Model-View-Component" approach for building UI state.
+//!
 //! `use_model` is basically just a fancy wrapper around set_state, but saves a "working copy" of the new state behind a
-//! RefCell. To modify the working copy, you need to call "modify" which returns the RefMut. This makes it easy to write
+//! RefCell. To modify the working copy, you need to call "get_mut" which returns the RefMut. This makes it easy to write
 //! fully encapsulated apps that retain a certain feel of native Rusty-ness. A calculator app is a good example of when this
 //! is useful.
+//!
+//! Do note that "get_mut" returns a `RefMut` (a lock over a RefCell). If two `RefMut`s are held at the same time (ie in a loop)
+//! the RefCell will panic and crash. You can use `try_get_mut` or `.modify` to avoid this problem, or just not hold two
+//! RefMuts at the same time.
 
 use dioxus::events::on::*;
 use dioxus::prelude::*;
-use std::{
-    cell::RefCell,
-    ops::{Deref, DerefMut},
-};
 
 fn main() {
     dioxus::webview::launch(App)
 }
 
 static App: FC<()> = |cx| {
-    let state = use_model(&cx, || CalculatorState::new());
+    let calc = use_model(&cx, || Calculator::new());
 
-    let clear_display = state.display_value.eq("0");
+    let clear_display = calc.display_value.eq("0");
     let clear_text = if clear_display { "C" } else { "AC" };
-    let formatted = state.formatted();
+    let formatted = calc.formatted();
 
     cx.render(rsx! {
-        div { class: "calculator", onkeydown: move |evt| state.modify().handle_keydown(evt),
-            div { class: "calculator-display", "{formatted}"}
+        div { class: "calculator", onkeydown: move |evt| calc.get_mut().handle_keydown(evt),
+            div { class: "calculator-display","{formatted}"}
             div { class: "input-keys"
                 div { class: "function-keys"
-                    CalculatorKey { name: "key-clear", onclick: move |_| state.modify().clear_display() "{clear_text}" }
-                    CalculatorKey { name: "key-sign", onclick: move |_| state.modify().toggle_sign(), "±"}
-                    CalculatorKey { name: "key-percent", onclick: move |_| state.modify().toggle_percent() "%"}
+                    // All there ways that the calculator may be modified:
+                    CalculatorKey { name: "key-clear", onclick: move |_| calc.get_mut().clear_display() "{clear_text}" }
+                    CalculatorKey { name: "key-sign", onclick: move |_| calc.modify(Calculator::toggle_sign), "±"}
+                    CalculatorKey { name: "key-percent", onclick: move |_| calc.modify(|f| f.toggle_percent()) "%"}
                 }
                 div { class: "digit-keys"
-                    CalculatorKey { name: "key-0", onclick: move |_| state.modify().input_digit(0), "0" }
-                    CalculatorKey { name: "key-dot", onclick: move |_|  state.modify().input_dot(), "●" }
-                    {(1..9).map(|k| rsx!{
-                        CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_|  state.modify().input_digit(k), "{k}" }
+                    CalculatorKey { name: "key-0", onclick: move |_| calc.get_mut().input_digit(0), "0" }
+                    CalculatorKey { name: "key-dot", onclick: move |_|  calc.get_mut().input_dot(), "●" }
+                    {(1..9).map(move |k| rsx!{
+                        CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_|  calc.get_mut().input_digit(k), "{k}" }
                     })}
                 }
                 div { class: "operator-keys"
-                    CalculatorKey { name:"key-divide", onclick: move |_| state.modify().set_operator(Operator::Div) "÷" }
-                    CalculatorKey { name:"key-multiply", onclick: move |_| state.modify().set_operator(Operator::Mul) "×" }
-                    CalculatorKey { name:"key-subtract", onclick: move |_| state.modify().set_operator(Operator::Sub) "−" }
-                    CalculatorKey { name:"key-add", onclick: move |_| state.modify().set_operator(Operator::Add) "+" }
-                    CalculatorKey { name:"key-equals", onclick: move |_| state.modify().perform_operation() "=" }
+                    CalculatorKey { name:"key-divide", onclick: move |_| calc.get_mut().set_operator(Operator::Div) "÷" }
+                    CalculatorKey { name:"key-multiply", onclick: move |_| calc.get_mut().set_operator(Operator::Mul) "×" }
+                    CalculatorKey { name:"key-subtract", onclick: move |_| calc.get_mut().set_operator(Operator::Sub) "−" }
+                    CalculatorKey { name:"key-add", onclick: move |_| calc.get_mut().set_operator(Operator::Add) "+" }
+                    CalculatorKey { name:"key-equals", onclick: move |_| calc.get_mut().perform_operation() "=" }
                 }
             }
         }
@@ -56,7 +59,7 @@ static App: FC<()> = |cx| {
 };
 
 #[derive(Clone)]
-struct CalculatorState {
+struct Calculator {
     display_value: String,
     operator: Option<Operator>,
     cur_val: f64,
@@ -68,9 +71,10 @@ enum Operator {
     Mul,
     Div,
 }
-impl CalculatorState {
+
+impl Calculator {
     fn new() -> Self {
-        CalculatorState {
+        Calculator {
             display_value: "0".to_string(),
             operator: None,
             cur_val: 0.0,
@@ -118,7 +122,9 @@ impl CalculatorState {
             self.display_value.pop();
         }
     }
-    fn set_operator(&mut self, operator: Operator) {}
+    fn set_operator(&mut self, operator: Operator) {
+        self.operator = Some(operator)
+    }
     fn handle_keydown(&mut self, evt: KeyboardEvent) {
         match evt.key_code() {
             KeyCode::Backspace => self.backspace(),
@@ -160,6 +166,10 @@ fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> VNode<'a> {
 use odl::use_model;
 mod odl {
     use super::*;
+    use std::{
+        cell::RefCell,
+        ops::{Deref, DerefMut},
+    };
     /// Use model makes it easy to use "models" as state for components. To modify the model, call "modify" and a clone of the
     /// current model will be made, with a RefMut lock on it. Dioxus will never run your components multithreaded, so you can
     /// be relatively sure that this won't fail in practice
@@ -208,9 +218,14 @@ mod odl {
                 wip: RefCell::new(t),
             }
         }
-        pub fn modify(&self) -> RefMut<'_, T> {
+        pub fn get_mut(&self) -> RefMut<'_, T> {
             self.wip.borrow_mut()
         }
+        pub fn modify(&self, f: impl FnOnce(&mut T)) {
+            let mut g = self.get_mut();
+            let r = g.deref_mut();
+            todo!()
+        }
     }
 
     #[derive(Clone, Copy)]

+ 9 - 1
examples/reference/iterators.rs

@@ -2,9 +2,17 @@ use dioxus::prelude::*;
 fn main() {}
 
 static Example: FC<()> = |cx| {
+    let (g, set_g) = use_state(&cx, || 0);
+    let v = (0..10).map(|f| {
+        rsx! {
+            li {
+                onclick: move |_| set_g(10)
+            }
+        }
+    });
     cx.render(rsx! {
         div {
-
+            {v}
         }
     })
 };

+ 7 - 0
examples/reference/main.rs

@@ -0,0 +1,7 @@
+// mod iterator;
+// mod memo;
+mod iterators;
+mod listener;
+mod memo;
+
+fn main() {}

+ 0 - 2
examples/reference/mod.rs

@@ -1,2 +0,0 @@
-mod iterator;
-mod memo;

+ 7 - 2
packages/cli/src/builder.rs

@@ -70,15 +70,20 @@ pub fn build(config: &Config, _build_config: &BuildConfig) -> Result<()> {
     // [3] Bindgen the final binary for use easy linking
     let mut bindgen_builder = Bindgen::new();
 
+    let release_type = match config.release {
+        true => "release",
+        false => "debug",
+    };
+
     let input_path = match executable {
         ExecutableType::Binary(name) | ExecutableType::Lib(name) => target_dir
             // .join("wasm32-unknown-unknown/release")
-            .join("wasm32-unknown-unknown/debug")
+            .join(format!("wasm32-unknown-unknown/{}", release_type))
             .join(format!("{}.wasm", name)),
 
         ExecutableType::Example(name) => target_dir
             // .join("wasm32-unknown-unknown/release/examples")
-            .join("wasm32-unknown-unknown/debug/examples")
+            .join(format!("wasm32-unknown-unknown/{}/examples", release_type))
             .join(format!("{}.wasm", name)),
     };
 

+ 30 - 6
packages/core-macro/src/rsx/element.rs

@@ -68,19 +68,43 @@ impl ToTokens for Element {
         let name = &self.name.to_string();
 
         tokens.append_all(quote! {
-            dioxus::builder::ElementBuilder::new(__cx, #name)
+            __cx.element(#name)
         });
+        // dioxus::builder::ElementBuilder::new(__cx, #name)
+        // dioxus::builder::ElementBuilder::new(__cx, #name)
 
+        // Add attributes
+        // TODO: conver to the "attrs" syntax for compile-time known sizes
         for attr in self.attrs.iter() {
             attr.to_tokens(tokens);
         }
 
-        let mut children = self.children.iter();
-        while let Some(child) = children.next() {
-            let inner_toks = child.to_token_stream();
+        // let mut children = self.children.iter();
+        // while let Some(child) = children.next() {
+        //     let inner_toks = child.to_token_stream();
+        //     tokens.append_all(quote! {
+        //         .iter_child(#inner_toks)
+        //     })
+        // }
+
+        let mut childs = quote! {};
+        for child in &self.children {
+            match child {
+                Node::Text(e) => e.to_tokens(&mut childs),
+                Node::Element(e) => e.to_tokens(&mut childs),
+                Node::RawExpr(e) => quote! {
+                    __cx.fragment_from_iter(#e)
+                }
+                .to_tokens(&mut childs),
+            }
+            childs.append_all(quote! {,})
+        }
+        if self.children.len() > 0 {
             tokens.append_all(quote! {
-                .iter_child(#inner_toks)
-            })
+                .children([
+                    #childs
+                ])
+            });
         }
 
         tokens.append_all(quote! {

+ 2 - 2
packages/core-macro/src/rsx/mod.rs

@@ -99,14 +99,14 @@ impl ToTokens for RsxRender {
             // The `in cx` pattern allows directly rendering
             Some(ident) => out_tokens.append_all(quote! {
                 #ident.render(dioxus::prelude::LazyNodes::new(move |__cx|{
-                    let bump = &__cx.bump();
+                    // let bump = &__cx.bump();
                     #inner
                 }))
             }),
             // Otherwise we just build the LazyNode wrapper
             None => out_tokens.append_all(quote! {
                 dioxus::prelude::LazyNodes::new(move |__cx: &NodeFactory|{
-                    let bump = &__cx.bump();
+                    // let bump = &__cx.bump();
                     #inner
                  })
             }),

+ 1 - 7
packages/core-macro/src/rsx/node.rs

@@ -61,13 +61,7 @@ impl ToTokens for TextNode {
         // todo: use heuristics to see if we can promote to &static str
         let token_stream = &self.0.to_token_stream();
         tokens.append_all(quote! {
-            {
-                // use bumpalo::core_alloc::fmt::Write;
-                // let mut s = bumpalo::collections::String::new_in(bump);
-                // s.write_fmt(format_args_f!(#token_stream)).unwrap();
-                dioxus::builder::text3(bump, format_args_f!(#token_stream))
-                // dioxus::builder::text2(s)
-            }
+            __cx.text(format_args_f!(#token_stream))
         });
     }
 }

+ 1 - 1
packages/core-macro/src/util.rs

@@ -105,7 +105,7 @@ static HTML_TAGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
         "source",
         "span",
         "strong",
-        // "style",
+        "style",
         "sub",
         "summary",
         "sup",

+ 360 - 191
packages/core/src/diff.rs

@@ -135,197 +135,357 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
         Then, we diff and queue an edit event (via chagelist). s single trees - when components show up, we save that traversal and then re-enter later.
         When re-entering, we reuse the EditList in DiffState
         */
-        match old_node {
-            VNode::Element(old) => match new_node {
-                // New node is an element, old node was en element, need to investiage more deeply
-                VNode::Element(new) => {
-                    // If the element type is completely different, the element needs to be re-rendered completely
-                    // This is an optimization React makes due to how users structure their code
-                    if new.tag_name != old.tag_name || new.namespace != old.namespace {
-                        self.create(new_node);
-                        self.dom.replace_with();
-                        return;
-                    }
-                    new.dom_id.set(old.dom_id.get());
-
-                    self.diff_listeners(old.listeners, new.listeners);
-                    self.diff_attr(old.attributes, new.attributes, new.namespace);
-                    self.diff_children(old.children, new.children);
-                }
-                // New node is a text element, need to replace the element with a simple text node
-                VNode::Text(_) => {
-                    self.create(new_node);
-                    self.dom.replace_with();
+        log::debug!("diffing...");
+        match (old_node, new_node) {
+            // Handle the "sane" cases first.
+            // The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment".
+            // So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable.
+            (VNode::Text(old), VNode::Text(new)) => {
+                if old.text != new.text {
+                    self.dom.push_root(old.dom_id.get());
+                    log::debug!("Text has changed {}, {}", old.text, new.text);
+                    self.dom.set_text(new.text);
                 }
+                new.dom_id.set(old.dom_id.get());
+            }
 
-                // New node is a component
-                // Make the component and replace our element on the stack with it
-                VNode::Component(_) => {
+            (VNode::Element(old), VNode::Element(new)) => {
+                // If the element type is completely different, the element needs to be re-rendered completely
+                // This is an optimization React makes due to how users structure their code
+                //
+                // In Dioxus, this is less likely to occur unless through a fragment
+                if new.tag_name != old.tag_name || new.namespace != old.namespace {
+                    log::debug!("things have changed....?");
+                    self.dom.push_root(old.dom_id.get());
                     self.create(new_node);
                     self.dom.replace_with();
+                    return;
                 }
+                log::debug!("Important Things haven't changed!");
+                new.dom_id.set(old.dom_id.get());
 
-                // New node is actually a sequence of nodes.
-                // We need to replace this one node with a sequence of nodes
-                // Not yet implement because it's kinda hairy
-                VNode::Fragment(new) => {
-                    match new.children.len() {
-                        0 => {
-                            // remove
-                        }
-                        1 => {
-                            // replace
-                            self.create(&new.children[0]);
-                            self.dom.replace_with();
-                        }
-                        _ => {
-                            todo!()
-                            // remove and mount many
-                            // self.remove_self_and_next_siblings(old)
-                            //
-                            // let iter = ChildIterator::new(new_node, self.components).rev();
-                            // for child in iter {}
-                            // for child in new.children.iter().skip(1).rev() {
-                            //     self.dom.remove();
-                            // }
-                        }
-                    }
-                }
+                self.diff_listeners(old.listeners, new.listeners);
+                self.diff_attr(old.attributes, new.attributes, new.namespace);
+                self.diff_children(old.children, new.children);
+            }
 
-                // New Node is actually suspended. Todo
-                VNode::Suspended { real } => todo!(),
-            },
+            (VNode::Component(old), VNode::Component(new)) => {
+                log::warn!("diffing components? {:#?}", new.user_fc);
+                if old.user_fc == new.user_fc {
+                    // Make sure we're dealing with the same component (by function pointer)
+
+                    // Make sure the new component vnode is referencing the right scope id
+                    let scope_id = old.ass_scope.get();
+                    new.ass_scope.set(scope_id);
+
+                    // new.mounted_root.set(old.mounted_root.get());
+
+                    // make sure the component's caller function is up to date
+                    let scope = self.components.try_get_mut(scope_id.unwrap()).unwrap();
+                    // .with_scope(scope_id.unwrap(), |scope| {
+                    scope.caller = Rc::downgrade(&new.caller);
+                    scope.update_children(new.children);
+                    // })
+                    // .unwrap();
+
+                    // React doesn't automatically memoize, but we do.
+                    // The cost is low enough to make it worth checking
+                    // Rust produces fairly performant comparison methods, sometimes SIMD accelerated
+                    // let should_render = match old.comparator {
+                    //     Some(comparator) => comparator(new),
+                    //     None => true,
+                    // };
+
+                    // if should_render {
+                    // // self.dom.commit_traversal();
+                    // let f = self.components.try_get_mut(scope_id.unwrap()).unwrap();
+                    // self.components
+                    //     .with_scope(scope_id.unwrap(), |f| {
+                    log::debug!("running scope during diff {:#?}", scope_id);
+                    scope.run_scope().unwrap();
+                    self.diff_node(scope.old_frame(), scope.next_frame());
+                    log::debug!("scope completed {:#?}", scope_id);
+                    self.seen_nodes.insert(scope_id.unwrap());
+                    // })
+                    // .unwrap();
+
+                    // diff_machine.change_list.load_known_root(root_id);
+                    // run the scope
+                    //
+                    // } else {
+                    //     log::error!("Memoized componented");
+                    //     // Component has memoized itself and doesn't need to be re-rendered.
+                    //     // We still need to make sure the child's props are up-to-date.
+                    //     // Don't commit traversal
+                    // }
+                } else {
+                    // It's an entirely different component
 
-            // Old element was text
-            VNode::Text(old) => match new_node {
-                VNode::Text(new) => {
-                    if old.text != new.text {
-                        log::debug!("Text has changed {}, {}", old.text, new.text);
-                        self.dom.set_text(new.text);
-                    }
-                    new.dom_id.set(old.dom_id.get());
-                }
-                VNode::Element(_) | VNode::Component(_) => {
+                    // A new component has shown up! We need to destroy the old node
+
+                    // Wipe the old one and plant the new one
+                    // self.dom.commit_traversal();
+                    // self.dom.replace_node_with(old.dom_id, new.dom_id);
+                    // self.create(new_node);
+                    log::warn!("creating and replacing...");
                     self.create(new_node);
-                    self.dom.replace_with();
-                }
 
-                // TODO on handling these types
-                VNode::Fragment(frag) => {
-                    if frag.children.len() == 0 {
-                        // do nothing
-                    } else {
-                        self.create(&frag.children[0]);
-                        self.dom.replace_with();
-                        for child in frag.children.iter().skip(1) {
-                            self.create(child);
-                            self.dom.append_child();
-                        }
-                    }
+                    // self.dom.replace_with();
+                    // self.create_and_repalce(new_node, old.mounted_root.get());
+
+                    // Now we need to remove the old scope and all of its descendents
+                    let old_scope = old.ass_scope.get().unwrap();
+                    self.destroy_scopes(old_scope);
                 }
-                VNode::Suspended { real } => todo!(),
-            },
+            }
 
-            // Old element was a component
-            VNode::Component(old) => {
-                match new_node {
-                    // It's something entirely different
-                    VNode::Element(_) | VNode::Text(_) => {
-                        self.create(new_node);
-                        self.dom.replace_with();
-                    }
+            (VNode::Fragment(old), VNode::Fragment(new)) => {
+                log::debug!("diffing as fragment");
+                // This is the case where options or direct vnodes might be used.
+                // In this case, it's faster to just skip ahead to their diff
+                if old.children.len() == 1 && new.children.len() == 1 {
+                    self.diff_node(old.children.get(0).unwrap(), new.children.get(0).unwrap());
+                    return;
+                }
 
-                    // It's also a component
-                    VNode::Component(new) => {
-                        if old.user_fc == new.user_fc {
-                            // Make sure we're dealing with the same component (by function pointer)
-
-                            // Make sure the new component vnode is referencing the right scope id
-                            let scope_id = old.ass_scope.get();
-                            new.ass_scope.set(scope_id);
-                            new.mounted_root.set(old.mounted_root.get());
-
-                            // make sure the component's caller function is up to date
-                            self.components
-                                .with_scope(scope_id.unwrap(), |scope| {
-                                    scope.caller = Rc::downgrade(&new.caller)
-                                })
-                                .unwrap();
-
-                            // React doesn't automatically memoize, but we do.
-                            // The cost is low enough to make it worth checking
-                            // Rust produces fairly performant comparison methods, sometimes SIMD accelerated
-                            let should_render = match old.comparator {
-                                Some(comparator) => comparator(new),
-                                None => true,
-                            };
-
-                            if should_render {
-                                // // self.dom.commit_traversal();
-                                self.components
-                                    .with_scope(scope_id.unwrap(), |f| {
-                                        f.run_scope().unwrap();
-                                    })
-                                    .unwrap();
-                                // diff_machine.change_list.load_known_root(root_id);
-                                // run the scope
-                                //
-                            } else {
-                                // Component has memoized itself and doesn't need to be re-rendered.
-                                // We still need to make sure the child's props are up-to-date.
-                                // Don't commit traversal
-                            }
-                        } else {
-                            // It's an entirely different component
+                // Diff using the approach where we're looking for added or removed nodes.
+                if old.children.len() != new.children.len() {}
 
-                            // A new component has shown up! We need to destroy the old node
+                // Diff where we think the elements are the same
+                if old.children.len() == new.children.len() {}
 
-                            // Wipe the old one and plant the new one
-                            // self.dom.commit_traversal();
-                            // self.dom.replace_node_with(old.dom_id, new.dom_id);
-                            // self.create(new_node);
-                            // self.dom.replace_with();
-                            self.create(new_node);
-                            // self.create_and_repalce(new_node, old.mounted_root.get());
-
-                            // Now we need to remove the old scope and all of its descendents
-                            let old_scope = old.ass_scope.get().unwrap();
-                            self.destroy_scopes(old_scope);
-                        }
-                    }
-                    VNode::Fragment(_) => todo!(),
-                    VNode::Suspended { real } => todo!(),
-                }
+                self.diff_children(old.children, new.children);
+                // todo!()
             }
 
-            VNode::Fragment(old) => {
-                //
-                match new_node {
-                    VNode::Fragment(new) => todo!(),
+            // Okay - these are the "insane" cases where the structure is entirely different.
+            // The factory and rsx! APIs don't really produce structures like this, so we don't take any too complicated
+            // code paths.
 
-                    // going from fragment to element means we're going from many (or potentially none) to one
-                    VNode::Element(new) => {}
-                    VNode::Text(_) => todo!(),
-                    VNode::Suspended { real } => todo!(),
-                    VNode::Component(_) => todo!(),
+            // in the case where the old node was a fragment but the new nodes are text,
+            (VNode::Fragment(_) | VNode::Component(_), VNode::Element(_) | VNode::Text(_)) => {
+                // find the first real element int the old node
+                let mut iter = RealChildIterator::new(old_node, self.components);
+                if let Some(first) = iter.next() {
+                    // replace the very first node with the creation of the element or text
+                } else {
+                    // there are no real elements in the old fragment...
+                    // We need to load up the next real
+                }
+                if let VNode::Component(old) = old_node {
+                    // schedule this component's destructor to be run
+                    todo!()
                 }
             }
-
-            // a suspended node will perform a mem-copy of the previous elements until it is ready
-            // this means that event listeners will need to be disabled and removed
-            // it also means that props will need to disabled - IE if the node "came out of hibernation" any props should be considered outdated
-            VNode::Suspended { real: old_real } => {
+            // In the case where real nodes are being replaced by potentially
+            (VNode::Element(_) | VNode::Text(_), VNode::Fragment(new)) => {
                 //
-                match new_node {
-                    VNode::Suspended { real: new_real } => {
-                        //
-                    }
-                    VNode::Element(_) => todo!(),
-                    VNode::Text(_) => todo!(),
-                    VNode::Fragment(_) => todo!(),
-                    VNode::Component(_) => todo!(),
-                }
             }
+            (VNode::Text(_), VNode::Element(_)) => {
+                self.create(new_node);
+                self.dom.replace_with();
+            }
+            (VNode::Element(_), VNode::Text(_)) => {
+                self.create(new_node);
+                self.dom.replace_with();
+            }
+
+            _ => {
+                //
+            } /*
+
+              */
+              //
+              // VNode::Element(old) => match new_node {
+              //     // New node is an element, old node was en element, need to investiage more deeply
+              //     VNode::Element(new) => {
+              //         // If the element type is completely different, the element needs to be re-rendered completely
+              //         // This is an optimization React makes due to how users structure their code
+              //         if new.tag_name != old.tag_name || new.namespace != old.namespace {
+              //             self.create(new_node);
+              //             self.dom.replace_with();
+              //             return;
+              //         }
+              //         new.dom_id.set(old.dom_id.get());
+
+              //         self.diff_listeners(old.listeners, new.listeners);
+              //         self.diff_attr(old.attributes, new.attributes, new.namespace);
+              //         self.diff_children(old.children, new.children);
+              //     }
+              //     // New node is a text element, need to replace the element with a simple text node
+              //     VNode::Text(_) => {
+              //         self.create(new_node);
+              //         self.dom.replace_with();
+              //     }
+
+              //     // New node is a component
+              //     // Make the component and replace our element on the stack with it
+              //     VNode::Component(_) => {
+              //         self.create(new_node);
+              //         self.dom.replace_with();
+              //     }
+
+              //     // New node is actually a sequence of nodes.
+              //     // We need to replace this one node with a sequence of nodes
+              //     // Not yet implement because it's kinda hairy
+              //     VNode::Fragment(new) => {
+              //         match new.children.len() {
+              //             0 => {
+              //                 // remove
+              //             }
+              //             1 => {
+              //                 // replace
+              //                 self.create(&new.children[0]);
+              //                 self.dom.replace_with();
+              //             }
+              //             _ => {
+              //                 todo!()
+              //                 // remove and mount many
+              //                 // self.remove_self_and_next_siblings(old)
+              //                 //
+              //                 // let iter = ChildIterator::new(new_node, self.components).rev();
+              //                 // for child in iter {}
+              //                 // for child in new.children.iter().skip(1).rev() {
+              //                 //     self.dom.remove();
+              //                 // }
+              //             }
+              //         }
+              //     }
+
+              //     // New Node is actually suspended. Todo
+              //     VNode::Suspended { real } => todo!(),
+              // },
+
+              // // Old element was text
+              // VNode::Text(old) => match new_node {
+              //     VNode::Text(new) => {
+              //         if old.text != new.text {
+              //             log::debug!("Text has changed {}, {}", old.text, new.text);
+              //             self.dom.set_text(new.text);
+              //         }
+              //         new.dom_id.set(old.dom_id.get());
+              //     }
+              //     VNode::Element(_) | VNode::Component(_) => {
+              //         self.create(new_node);
+              //         self.dom.replace_with();
+              //     }
+
+              //     // TODO on handling these types
+              //     VNode::Fragment(frag) => {
+              //         if frag.children.len() == 0 {
+              //             // do nothing
+              //         } else {
+              //             self.create(&frag.children[0]);
+              //             self.dom.replace_with();
+              //             for child in frag.children.iter().skip(1) {
+              //                 self.create(child);
+              //                 self.dom.append_child();
+              //             }
+              //         }
+              //     }
+              //     VNode::Suspended { real } => todo!(),
+              // },
+
+              // // Old element was a component
+              // VNode::Component(old) => {
+              //     match new_node {
+              //         // It's something entirely different
+              //         VNode::Element(_) | VNode::Text(_) => {
+              //             self.create(new_node);
+              //             self.dom.replace_with();
+              //         }
+
+              //         // It's also a component
+              //         VNode::Component(new) => {
+              //             if old.user_fc == new.user_fc {
+              //                 // Make sure we're dealing with the same component (by function pointer)
+
+              //                 // Make sure the new component vnode is referencing the right scope id
+              //                 let scope_id = old.ass_scope.get();
+              //                 new.ass_scope.set(scope_id);
+              //                 new.mounted_root.set(old.mounted_root.get());
+
+              //                 // make sure the component's caller function is up to date
+              //                 self.components
+              //                     .with_scope(scope_id.unwrap(), |scope| {
+              //                         scope.caller = Rc::downgrade(&new.caller)
+              //                     })
+              //                     .unwrap();
+
+              //                 // React doesn't automatically memoize, but we do.
+              //                 // The cost is low enough to make it worth checking
+              //                 // Rust produces fairly performant comparison methods, sometimes SIMD accelerated
+              //                 let should_render = match old.comparator {
+              //                     Some(comparator) => comparator(new),
+              //                     None => true,
+              //                 };
+
+              //                 if should_render {
+              //                     // // self.dom.commit_traversal();
+              //                     self.components
+              //                         .with_scope(scope_id.unwrap(), |f| {
+              //                             f.run_scope().unwrap();
+              //                         })
+              //                         .unwrap();
+              //                     // diff_machine.change_list.load_known_root(root_id);
+              //                     // run the scope
+              //                     //
+              //                 } else {
+              //                     // Component has memoized itself and doesn't need to be re-rendered.
+              //                     // We still need to make sure the child's props are up-to-date.
+              //                     // Don't commit traversal
+              //                 }
+              //             } else {
+              //                 // It's an entirely different component
+
+              //                 // A new component has shown up! We need to destroy the old node
+
+              //                 // Wipe the old one and plant the new one
+              //                 // self.dom.commit_traversal();
+              //                 // self.dom.replace_node_with(old.dom_id, new.dom_id);
+              //                 // self.create(new_node);
+              //                 // self.dom.replace_with();
+              //                 self.create(new_node);
+              //                 // self.create_and_repalce(new_node, old.mounted_root.get());
+
+              //                 // Now we need to remove the old scope and all of its descendents
+              //                 let old_scope = old.ass_scope.get().unwrap();
+              //                 self.destroy_scopes(old_scope);
+              //             }
+              //         }
+              //         VNode::Fragment(_) => todo!(),
+              //         VNode::Suspended { real } => todo!(),
+              //     }
+              // }
+
+              // VNode::Fragment(old) => {
+              //     //
+              //     match new_node {
+              //         VNode::Fragment(new) => todo!(),
+
+              //         // going from fragment to element means we're going from many (or potentially none) to one
+              //         VNode::Element(new) => {}
+              //         VNode::Text(_) => todo!(),
+              //         VNode::Suspended { real } => todo!(),
+              //         VNode::Component(_) => todo!(),
+              //     }
+              // }
+
+              // // a suspended node will perform a mem-copy of the previous elements until it is ready
+              // // this means that event listeners will need to be disabled and removed
+              // // it also means that props will need to disabled - IE if the node "came out of hibernation" any props should be considered outdated
+              // VNode::Suspended { real: old_real } => {
+              //     //
+              //     match new_node {
+              //         VNode::Suspended { real: new_real } => {
+              //             //
+              //         }
+              //         VNode::Element(_) => todo!(),
+              //         VNode::Text(_) => todo!(),
+              //         VNode::Fragment(_) => todo!(),
+              //         VNode::Component(_) => todo!(),
+              //     }
+              // }
         }
     }
 
@@ -340,6 +500,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
     //     [... node]
     fn create(&mut self, node: &'bump VNode<'bump>) {
         // debug_assert!(self.dom.traversal_is_committed());
+        log::warn!("Creating node!");
         match node {
             VNode::Text(text) => {
                 let real_id = self.dom.create_text_node(text.text);
@@ -402,7 +563,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
             }
 
             VNode::Component(component) => {
-                let real_id = self.dom.create_placeholder();
+                // let real_id = self.dom.create_placeholder();
 
                 // let root_id = next_id();
                 // self.dom.save_known_root(root_id);
@@ -455,6 +616,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
                 new_component.run_scope().unwrap();
 
                 // And then run the diff algorithm
+                let _real_id = self.dom.create_placeholder();
                 self.diff_node(new_component.old_frame(), new_component.next_frame());
 
                 // Finally, insert this node as a seen node.
@@ -675,11 +837,14 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
         );
 
         if new_is_keyed && old_is_keyed {
-            todo!("Not yet implemented a migration away from temporaries");
+            log::warn!("using the wrong approach");
+            self.diff_non_keyed_children(old, new);
+            // todo!("Not yet implemented a migration away from temporaries");
             // let t = self.dom.next_temporary();
             // self.diff_keyed_children(old, new);
             // self.dom.set_next_temporary(t);
         } else {
+            log::debug!("diffing non keyed children");
             self.diff_non_keyed_children(old, new);
         }
     }
@@ -1120,24 +1285,24 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
             // self.dom.go_to_sibling(i);
             // [... parent this_child]
 
-            let did = old_child.get_mounted_id(self.components).unwrap();
-            if did.0 == 0 {
-                log::debug!("Root is bad: {:#?}", old_child);
-            }
-            self.dom.push_root(did);
+            // let did = old_child.get_mounted_id(self.components).unwrap();
+            // if did.0 == 0 {
+            //     log::debug!("Root is bad: {:#?}", old_child);
+            // }
+            // self.dom.push_root(did);
             self.diff_node(old_child, new_child);
 
-            let old_id = old_child.get_mounted_id(self.components).unwrap();
-            let new_id = new_child.get_mounted_id(self.components).unwrap();
-
-            log::debug!(
-                "pushed root. {:?}, {:?}",
-                old_child.get_mounted_id(self.components).unwrap(),
-                new_child.get_mounted_id(self.components).unwrap()
-            );
-            if old_id != new_id {
-                log::debug!("Mismatch: {:?}", new_child);
-            }
+            // let old_id = old_child.get_mounted_id(self.components).unwrap();
+            // let new_id = new_child.get_mounted_id(self.components).unwrap();
+
+            // log::debug!(
+            //     "pushed root. {:?}, {:?}",
+            //     old_child.get_mounted_id(self.components).unwrap(),
+            //     new_child.get_mounted_id(self.components).unwrap()
+            // );
+            // if old_id != new_id {
+            //     log::debug!("Mismatch: {:?}", new_child);
+            // }
         }
 
         // match old.len().cmp(&new.len()) {
@@ -1256,7 +1421,11 @@ enum KeyedPrefixResult {
     MoreWorkToDo(usize),
 }
 
-struct ChildIterator<'a> {
+/// This iterator iterates through a list of virtual children and only returns real children (Elements or Text).
+///
+/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like
+/// "InsertBefore".
+struct RealChildIterator<'a> {
     scopes: &'a ScopeArena,
 
     // Heuristcally we should never bleed into 5 completely nested fragments/components
@@ -1264,7 +1433,7 @@ struct ChildIterator<'a> {
     stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>,
 }
 
-impl<'a> ChildIterator<'a> {
+impl<'a> RealChildIterator<'a> {
     fn new(starter: &'a VNode<'a>, scopes: &'a ScopeArena) -> Self {
         Self {
             scopes,
@@ -1279,7 +1448,7 @@ impl<'a> ChildIterator<'a> {
 //     }
 // }
 
-impl<'a> Iterator for ChildIterator<'a> {
+impl<'a> Iterator for RealChildIterator<'a> {
     type Item = &'a VNode<'a>;
 
     fn next(&mut self) -> Option<&'a VNode<'a>> {
@@ -1402,7 +1571,7 @@ mod tests {
         let mut renderer = DebugDom::new();
         dom.rebuild(&mut renderer).unwrap();
         let starter = dom.base_scope().root();
-        let ite = ChildIterator::new(starter, &dom.components);
+        let ite = RealChildIterator::new(starter, &dom.components);
         for child in ite {
             match child {
                 VNode::Element(el) => println!("Found: Element {}", el.tag_name),

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

@@ -10,14 +10,62 @@ use crate::{innerlude::ScopeIdx, virtual_dom::RealDomNode};
 
 #[derive(Debug)]
 pub struct EventTrigger {
+    ///
     pub component_id: ScopeIdx,
+
+    ///
     pub real_node_id: RealDomNode,
+
+    ///
     pub event: VirtualEvent,
+
+    ///
+    pub priority: EventPriority,
+}
+
+/// Priority of Event Triggers.
+///
+/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus
+/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers
+/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well.
+///
+/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
+#[derive(Debug)]
+pub enum EventPriority {
+    /// "Immediate" work will interrupt whatever work is currently being done and force its way through. This type of work
+    /// is typically reserved for small changes to single elements.
+    ///
+    /// The primary user of the "Immediate" priority is the `Signal` API which performs surgical mutations to the DOM.
+    Immediate,
+
+    /// "High Priority" work will not interrupt other high priority work, but will interrupt long medium and low priority work.
+    ///
+    ///
+    /// This is typically reserved for things like user interaction.
+    High,
+
+    /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important
+    /// than "High Priority" events and will take presedence over low priority events.
+    ///
+    /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input.
+    Medium,
+
+    /// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be
+    /// advanced to the front of the work queue until completed.
+    ///
+    /// The primary user of Low Priority work is the asynchronous work system (suspense).
+    Low,
 }
 
 impl EventTrigger {
-    pub fn new(event: VirtualEvent, scope: ScopeIdx, mounted_dom_id: RealDomNode) -> Self {
+    pub fn new(
+        event: VirtualEvent,
+        scope: ScopeIdx,
+        mounted_dom_id: RealDomNode,
+        priority: EventPriority,
+    ) -> Self {
         Self {
+            priority,
             component_id: scope,
             real_node_id: mounted_dom_id,
             event,
@@ -78,7 +126,7 @@ pub mod on {
         ( $( $eventdata:ident($wrapper:ident): [ $( $name:ident )* ]; )* ) => {
             $(
                 #[derive(Debug)]
-                pub struct $wrapper(Rc<dyn $eventdata>);
+                pub struct $wrapper(pub Rc<dyn $eventdata>);
                 impl Deref for $wrapper {
                     type Target = Rc<dyn $eventdata>;
                     fn deref(&self) -> &Self::Target {

+ 24 - 6
packages/core/src/nodebuilder.rs

@@ -9,6 +9,8 @@ use std::{
     u128,
 };
 
+use bumpalo::Bump;
+
 use crate::{
     events::VirtualEvent,
     innerlude::{Properties, VComponent, FC},
@@ -654,8 +656,9 @@ pub fn text3<'a>(bump: &'a bumpalo::Bump, args: std::fmt::Arguments) -> VNode<'a
     // we can just short-circuit to the &'static str version instead of having to allocate in the bump arena.
     //
     // In the most general case, this prevents the need for any string allocations for simple code IE:
-    //      div {"abc"}
-    //
+    // ```
+    //  div {"abc"}
+    // ```
     match args.as_str() {
         Some(static_str) => VNode::text(static_str),
         None => {
@@ -735,13 +738,14 @@ impl<'a, 'b> ChildrenList<'a, 'b> {
     }
 }
 
-// NodeFactory is used to build VNodes in the component's memory space.
-// This struct adds metadata to the final VNode about listeners, attributes, and children
+/// This struct provides an ergonomic API to quickly build VNodes.
+///
+/// NodeFactory is used to build VNodes in the component's memory space.
+/// This struct adds metadata to the final VNode about listeners, attributes, and children
 #[derive(Clone)]
 pub struct NodeFactory<'a> {
     pub scope_ref: &'a Scope,
     pub listener_id: Cell<usize>,
-    // pub listener_id: RefCell<usize>,
 }
 
 impl<'a> NodeFactory<'a> {
@@ -756,7 +760,7 @@ impl<'a> NodeFactory<'a> {
     }
 
     /// Create an element builder
-    pub fn element<'b, Listeners, Attributes, Children>(
+    pub fn element<'b>(
         &'b self,
         tag_name: &'static str,
     ) -> ElementBuilder<
@@ -787,6 +791,20 @@ impl<'a> NodeFactory<'a> {
             key: NodeKey::new_opt(key),
         }))
     }
+
+    pub fn fragment_from_iter(
+        &self,
+        node_iter: impl IntoIterator<Item = impl IntoVNode<'a>>,
+    ) -> VNode<'a> {
+        let mut nodes = bumpalo::collections::Vec::new_in(self.bump());
+        for node in node_iter.into_iter() {
+            nodes.push(node.into_vnode(&self));
+        }
+        VNode::Fragment(
+            self.bump()
+                .alloc(VFragment::new(None, nodes.into_bump_slice())),
+        )
+    }
 }
 
 use std::fmt::Debug;

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

@@ -29,7 +29,11 @@ pub enum VNode<'src> {
     /// A text node (node type `TEXT_NODE`).
     Text(VText<'src>),
 
-    /// A fragment is a "virtual position" in the DOM
+    /// A fragment is a list of elements that might have a dynamic order.
+    /// Normally, children will have a fixed order. However, Fragments allow a dynamic order and must be diffed differently.
+    ///
+    /// Fragments don't have a single mount into the dom, so their position is characterized by the head and tail nodes.
+    ///
     /// Fragments may have children and keys
     Fragment(&'src VFragment<'src>),
 

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

@@ -487,6 +487,14 @@ impl Scope {
         self.caller = broken_caller;
     }
 
+    pub fn update_children<'creator_node>(
+        &mut self,
+        child_nodes: &'creator_node [VNode<'creator_node>],
+    ) {
+        let child_nodes = unsafe { std::mem::transmute(child_nodes) };
+        self.child_nodes = child_nodes;
+    }
+
     /// 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
     ///
@@ -597,9 +605,9 @@ impl Scope {
 ///
 /// }
 ///
-/// fn example(cx: Context, props: &Props -> VNode {
+/// fn example(cx: Context<Props>) -> VNode {
 ///     html! {
-///         <div> "Hello, {cx.cx.name}" </div>
+///         <div> "Hello, {cx.name}" </div>
 ///     }
 /// }
 /// ```

+ 4 - 1
packages/web/Cargo.toml

@@ -10,7 +10,7 @@ license = "MIT/Apache-2.0"
 [dependencies]
 dioxus-core = { path="../core", version="0.1.2" }
 js-sys = "0.3"
-wasm-bindgen = "0.2.71"
+wasm-bindgen = { version="0.2.71", features=["enable-interning"] }
 lazy_static = "1.4.0"
 wasm-bindgen-futures = "0.4.20"
 wasm-logger = "0.2.0"
@@ -31,6 +31,7 @@ slotmap = "1.0.3"
 version = "0.3.50"
 features = [
     "Comment",
+    "Attr",
     "Document",
     "Element",
     "HtmlElement",
@@ -47,6 +48,7 @@ features = [
     "MouseEvent",
     "InputEvent",
     "ClipboardEvent",
+    "NamedNodeMap",
     "KeyboardEvent",
     "TouchEvent",
     "WheelEvent",
@@ -70,4 +72,5 @@ crate-type = ["cdylib", "rlib"]
 [dev-dependencies]
 im-rc = "15.0.0"
 rand = { version="0.8.4", features=["small_rng"] }
+separator = "0.4.1"
 uuid = { version="0.8.2", features=["v4", "wasm-bindgen"] }

+ 6 - 0
packages/web/README.md

@@ -3,3 +3,9 @@
 Build interactive user experiences directly in the web browser!
 
 Dioxus-web provides a `WebsysRenderer` for the Dioxus Virtual Dom that handles events, progresses components, and updates the actual DOM using web-sys methods.
+
+
+## Web-specific Optimizations
+- Uses string interning of all common node types
+- Optimistically interns short strings
+- Builds trees completely before mounting them

+ 29 - 42
packages/web/examples/basic.rs

@@ -1,5 +1,6 @@
 //! Basic example that renders a simple VNode to the browser.
 
+use dioxus::events::on::MouseEvent;
 use dioxus_core as dioxus;
 use dioxus_core::prelude::*;
 use dioxus_web::*;
@@ -14,56 +15,42 @@ fn main() {
 }
 
 static App: FC<()> = |cx| {
+    let (state, set_state) = use_state(&cx, || 0);
     cx.render(rsx! {
         div {
-            h1 {"hello"}
-            C1 {}
-            C2 {}
+            section { class: "py-12 px-4 text-center"
+                div { class: "w-full max-w-2xl mx-auto"
+                    span { class: "text-sm font-semibold"
+                        "count: {state}"
+                    }
+                    div {
+                        C1 {
+                            onclick: move |_| set_state(state + 1)
+                            "incr"
+                        }
+                        C1 {
+                            onclick: move |_| set_state(state - 1)
+                            "decr"
+                        }
+                    }
+                }
+            }
         }
     })
 };
 
-static C1: FC<()> = |cx| {
-    cx.render(rsx! {
-        button {
-            "numba 1"
-        }
-    })
-};
+#[derive(Props)]
+struct IncrementerProps<'a> {
+    onclick: &'a dyn Fn(MouseEvent),
+}
 
-static C2: FC<()> = |cx| {
+fn C1<'a, 'b>(cx: Context<'a, IncrementerProps<'b>>) -> VNode<'a> {
     cx.render(rsx! {
         button {
-            "numba 2"
+            class: "inline-block py-4 px-8 mr-6 leading-none text-white bg-indigo-600 hover:bg-indigo-900 font-semibold rounded shadow"
+            onclick: {cx.onclick}
+            "becr"
+            {cx.children()}
         }
     })
-};
-
-static DocExamples: FC<()> = |cx| {
-    //
-
-    let is_ready = false;
-
-    let items = (0..10).map(|i| rsx! { li {"{i}"} });
-    let _ = rsx! {
-        ul {
-            {items}
-        }
-    };
-
-    // rsx! {
-    //     div {}
-    //     h1 {}
-    //     {""}
-    //     "asbasd"
-    //     dioxus::Fragment {
-    //         //
-    //     }
-    // }
-
-    cx.render(rsx! {
-        div {
-            { is_ready.then(|| rsx!{ h1 {"We are ready!"} }) }
-        }
-    })
-};
+}

+ 181 - 0
packages/web/examples/calculator.rs

@@ -0,0 +1,181 @@
+//! Example: Calculator
+//! -------------------------
+
+// use dioxus::events::on::*;
+// use dioxus::prelude::*;
+
+use dioxus_core as dioxus;
+use dioxus_web::WebsysRenderer;
+
+const STYLE: &str = include_str!("../../../examples/assets/calculator.css");
+
+fn main() {
+    // Setup logging
+    wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
+    console_error_panic_hook::set_once();
+
+    // Run the app
+    wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App));
+}
+
+use dioxus::events::on::*;
+use dioxus::prelude::*;
+
+enum Operator {
+    Add,
+    Sub,
+    Mul,
+    Div,
+}
+
+static App: FC<()> = |cx| {
+    let (cur_val, set_cur_val) = use_state(&cx, || 0.0_f64);
+    let (operator, set_operator) = use_state(&cx, || None as Option<Operator>);
+    let (display_value, set_display_value) = use_state(&cx, || "".to_string());
+
+    let clear_display = display_value.eq("0");
+    let clear_text = if clear_display { "C" } else { "AC" };
+
+    let input_digit =
+        move |num: u8| {
+            log::warn!("Inputting digit {:#?}", num);
+            let mut new = display_value.clone();
+            new.push_str(num.to_string().as_str());
+            set_display_value(new);
+        };
+
+    let input_dot = move || {
+        let mut new = display_value.clone();
+        new.push_str(".");
+        set_display_value(new);
+    };
+
+    let perform_operation = move || {
+        if let Some(op) = operator.as_ref() {
+            let rhs = display_value.parse::<f64>().unwrap();
+            let new_val = match op {
+                Operator::Add => *cur_val + rhs,
+                Operator::Sub => *cur_val - rhs,
+                Operator::Mul => *cur_val * rhs,
+                Operator::Div => *cur_val / rhs,
+            };
+            set_cur_val(new_val);
+            set_display_value(new_val.to_string());
+            set_operator(None);
+        }
+    };
+
+    let toggle_sign = move |_| {
+        if display_value.starts_with("-") {
+            set_display_value(display_value.trim_start_matches("-").to_string())
+        } else {
+            set_display_value(format!("-{}", *display_value))
+        }
+    };
+    let toggle_percent = move |_| todo!();
+
+    let clear_key = move |_| {
+        set_display_value("0".to_string());
+        if !clear_display {
+            set_operator(None);
+            set_cur_val(0.0);
+        }
+    };
+
+    let keydownhandler = move |evt: KeyboardEvent| match evt.key_code() {
+        KeyCode::Backspace => {
+            //
+            let mut new = display_value.clone();
+            if !new.as_str().eq("0") {
+                new.pop();
+            }
+            set_display_value(new);
+
+        },
+        KeyCode::_0 => input_digit(0),
+        KeyCode::_1 => input_digit(1),
+        KeyCode::_2 => input_digit(2),
+        KeyCode::_3 => input_digit(3),
+        KeyCode::_4 => input_digit(4),
+        KeyCode::_5 => input_digit(5),
+        KeyCode::_6 => input_digit(6),
+        KeyCode::_7 => input_digit(7),
+        KeyCode::_8 => input_digit(8),
+        KeyCode::_9 => input_digit(9),
+        KeyCode::Add => set_operator(Some(Operator::Add)),
+        KeyCode::Subtract => set_operator(Some(Operator::Sub)),
+        KeyCode::Divide => set_operator(Some(Operator::Div)),
+        KeyCode::Multiply => set_operator(Some(Operator::Mul)),
+        _ => {}
+    };
+
+    cx.render(rsx! {
+        div { class: "app"
+            onkeydown: {keydownhandler}
+            style {
+                "{STYLE}"
+            }
+            div { class: "calculator", 
+                div { class: "input-keys"
+                    CalculatorDisplay { val: *cur_val }
+                    div { class: "function-keys"
+                        CalculatorKey { name: "key-clear", onclick: {clear_key} "{clear_text}" }
+                        CalculatorKey { name: "key-sign", onclick: {toggle_sign}, "±"}
+                        CalculatorKey { name: "key-percent", onclick: {toggle_percent} "%"}
+                    }
+                    div { class: "digit-keys"
+                        CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" }
+                        CalculatorKey { name: "key-dot", onclick: move |_|  input_dot(), "●" }
+    
+                        {(1..9).map(move |k| rsx!{
+                            CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_|  input_digit(k), "{k}" }
+                        })}
+                    }
+                    div { class: "operator-keys"
+                        CalculatorKey { name: "key-divide", onclick: move |_| set_operator(Some(Operator::Div)) "÷" }
+                        CalculatorKey { name: "key-multiply", onclick: move |_| set_operator(Some(Operator::Mul)) "×" }
+                        CalculatorKey { name: "key-subtract", onclick: move |_| set_operator(Some(Operator::Sub)) "−" }
+                        CalculatorKey { name: "key-add", onclick: move |_| set_operator(Some(Operator::Add)) "+" }
+                        CalculatorKey { name: "key-equals", onclick: move |_| perform_operation() "=" }
+                    }
+                }
+            }
+        }
+    })
+};
+
+#[derive(Props)]
+struct CalculatorKeyProps<'a> {
+    /// Name!
+    name: &'static str,
+
+    /// Click!
+    onclick: &'a dyn Fn(MouseEvent),
+}
+
+fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> VNode<'a> {
+    cx.render(rsx! {
+        button {
+            class: "calculator-key {cx.name}"
+            onclick: {cx.onclick}
+            {cx.children()}
+        }
+    })
+}
+
+#[derive(Props, PartialEq)]
+struct CalculatorDisplayProps {
+    val: f64,
+}
+
+fn CalculatorDisplay(cx: Context<CalculatorDisplayProps>) -> VNode {
+    use separator::Separatable;
+    // Todo, add float support to the num-format crate
+    let formatted = cx.val.separated_string();
+    // TODO: make it autoscaling with css
+    cx.render(rsx! {
+        div { class: "calculator-display"
+            "{formatted}"
+        }
+    })
+}

+ 6 - 4
packages/web/examples/hello.rs

@@ -3,15 +3,17 @@ use dioxus_core::prelude::*;
 use dioxus_web::WebsysRenderer;
 
 fn main() {
-    wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
-    console_error_panic_hook::set_once();
+    // wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
+    // console_error_panic_hook::set_once();
+
+    // log::info!("hello world");
+    dioxus_web::intern_cache();
 
-    log::info!("hello world");
     wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example));
 }
 
 static Example: FC<()> = |cx| {
-    let nodes = (0..15).map(|f| rsx! (li { key: "{f}", "{f}"}));
+    let nodes = (0..500).map(|f| rsx! (li {"div"}));
     cx.render(rsx! {
         div {
             span {

+ 294 - 44
packages/web/src/lib.rs

@@ -77,49 +77,6 @@ impl WebsysRenderer {
                 .progress_with_event(&mut websys_dom, trigger)?;
         }
 
-        // let edits = self.internal_dom.rebuild()?;
-        // log::debug!("Received edits: {:#?}", edits);
-        // edits.iter().for_each(|edit| {
-        //     log::debug!("patching with  {:?}", edit);
-        //     patch_machine.handle_edit(edit);
-        // });
-
-        // patch_machine.reset();
-        // let root_node = body_element.first_child().unwrap();
-        // patch_machine.stack.push(root_node.clone());
-
-        // log::debug!("patch stack size {:?}", patch_machine.stack);
-
-        // Event loop waits for the receiver to finish up
-        // TODO! Connect the sender to the virtual dom's suspense system
-        // Suspense is basically an external event that can force renders to specific nodes
-        // while let Ok(event) = receiver.recv().await {
-        // log::debug!("Stack before entrance {:#?}", patch_machine.stack.top());
-        // log::debug!("patch stack size before {:#?}", patch_machine.stack);
-        // patch_machine.reset();
-        // patch_machine.stack.push(root_node.clone());
-        // self.internal_dom
-        //     .progress_with_event(&mut websys_dom, event)?;
-        // let edits = self.internal_dom.progress_with_event(event)?;
-        // log::debug!("Received edits: {:#?}", edits);
-
-        // for edit in &edits {
-        //     // log::debug!("edit stream {:?}", edit);
-        //     // log::debug!("Stream stack {:#?}", patch_machine.stack.top());
-        //     patch_machine.handle_edit(edit);
-        // }
-
-        // log::debug!("patch stack size after {:#?}", patch_machine.stack);
-        // patch_machine.reset();
-        // our root node reference gets invalidated
-        // not sure why
-        // for now, just select the first child again.
-        // eventually, we'll just make our own root element instead of using body
-        // or just use body directly IDEK
-        // let root_node = body_element.first_child().unwrap();
-        // patch_machine.stack.push(root_node.clone());
-        // }
-
         Ok(()) // should actually never return from this, should be an error, rustc just cant see it
     }
 }
@@ -135,7 +92,7 @@ fn prepare_websys_dom() -> Element {
 
     // Build a dummy div
     let container: &Element = body.as_ref();
-    container.set_inner_html("");
+    // container.set_inner_html("");
     container
         .append_child(
             document
@@ -164,6 +121,299 @@ fn prepare_websys_dom() -> Element {
 //     };
 // }
 
+/// Wasm-bindgen has a performance option to intern commonly used phrases
+/// This saves the decoding cost, making the interaction of Rust<->JS more performant.
+/// We intern all the HTML tags and attributes, making most operations much faster.
+///
+/// Interning takes about 1ms at the start of the app, but saves a *ton* of time later on.
+pub fn intern_cache() {
+    let cached_words = [
+        // All the HTML Tags
+        "a",
+        "abbr",
+        "address",
+        "area",
+        "article",
+        "aside",
+        "audio",
+        "b",
+        "base",
+        "bdi",
+        "bdo",
+        "big",
+        "blockquote",
+        "body",
+        "br",
+        "button",
+        "canvas",
+        "caption",
+        "cite",
+        "code",
+        "col",
+        "colgroup",
+        "command",
+        "data",
+        "datalist",
+        "dd",
+        "del",
+        "details",
+        "dfn",
+        "dialog",
+        "div",
+        "dl",
+        "dt",
+        "em",
+        "embed",
+        "fieldset",
+        "figcaption",
+        "figure",
+        "footer",
+        "form",
+        "h1",
+        "h2",
+        "h3",
+        "h4",
+        "h5",
+        "h6",
+        "head",
+        "header",
+        "hr",
+        "html",
+        "i",
+        "iframe",
+        "img",
+        "input",
+        "ins",
+        "kbd",
+        "keygen",
+        "label",
+        "legend",
+        "li",
+        "link",
+        "main",
+        "map",
+        "mark",
+        "menu",
+        "menuitem",
+        "meta",
+        "meter",
+        "nav",
+        "noscript",
+        "object",
+        "ol",
+        "optgroup",
+        "option",
+        "output",
+        "p",
+        "param",
+        "picture",
+        "pre",
+        "progress",
+        "q",
+        "rp",
+        "rt",
+        "ruby",
+        "s",
+        "samp",
+        "script",
+        "section",
+        "select",
+        "small",
+        "source",
+        "span",
+        "strong",
+        "style",
+        "sub",
+        "summary",
+        "sup",
+        "table",
+        "tbody",
+        "td",
+        "textarea",
+        "tfoot",
+        "th",
+        "thead",
+        "time",
+        "title",
+        "tr",
+        "track",
+        "u",
+        "ul",
+        "var",
+        "video",
+        "wbr",
+        // All the event handlers
+        "Attribute",
+        "accept",
+        "accept-charset",
+        "accesskey",
+        "action",
+        "alt",
+        "async",
+        "autocomplete",
+        "autofocus",
+        "autoplay",
+        "charset",
+        "checked",
+        "cite",
+        "class",
+        "cols",
+        "colspan",
+        "content",
+        "contenteditable",
+        "controls",
+        "coords",
+        "data",
+        "data-*",
+        "datetime",
+        "default",
+        "defer",
+        "dir",
+        "dirname",
+        "disabled",
+        "download",
+        "draggable",
+        "enctype",
+        "for",
+        "form",
+        "formaction",
+        "headers",
+        "height",
+        "hidden",
+        "high",
+        "href",
+        "hreflang",
+        "http-equiv",
+        "id",
+        "ismap",
+        "kind",
+        "label",
+        "lang",
+        "list",
+        "loop",
+        "low",
+        "max",
+        "maxlength",
+        "media",
+        "method",
+        "min",
+        "multiple",
+        "muted",
+        "name",
+        "novalidate",
+        "onabort",
+        "onafterprint",
+        "onbeforeprint",
+        "onbeforeunload",
+        "onblur",
+        "oncanplay",
+        "oncanplaythrough",
+        "onchange",
+        "onclick",
+        "oncontextmenu",
+        "oncopy",
+        "oncuechange",
+        "oncut",
+        "ondblclick",
+        "ondrag",
+        "ondragend",
+        "ondragenter",
+        "ondragleave",
+        "ondragover",
+        "ondragstart",
+        "ondrop",
+        "ondurationchange",
+        "onemptied",
+        "onended",
+        "onerror",
+        "onfocus",
+        "onhashchange",
+        "oninput",
+        "oninvalid",
+        "onkeydown",
+        "onkeypress",
+        "onkeyup",
+        "onload",
+        "onloadeddata",
+        "onloadedmetadata",
+        "onloadstart",
+        "onmousedown",
+        "onmousemove",
+        "onmouseout",
+        "onmouseover",
+        "onmouseup",
+        "onmousewheel",
+        "onoffline",
+        "ononline",
+        "<body>",
+        "onpageshow",
+        "onpaste",
+        "onpause",
+        "onplay",
+        "onplaying",
+        "<body>",
+        "onprogress",
+        "onratechange",
+        "onreset",
+        "onresize",
+        "onscroll",
+        "onsearch",
+        "onseeked",
+        "onseeking",
+        "onselect",
+        "onstalled",
+        "<body>",
+        "onsubmit",
+        "onsuspend",
+        "ontimeupdate",
+        "ontoggle",
+        "onunload",
+        "onvolumechange",
+        "onwaiting",
+        "onwheel",
+        "open",
+        "optimum",
+        "pattern",
+        "placeholder",
+        "poster",
+        "preload",
+        "readonly",
+        "rel",
+        "required",
+        "reversed",
+        "rows",
+        "rowspan",
+        "sandbox",
+        "scope",
+        "selected",
+        "shape",
+        "size",
+        "sizes",
+        "span",
+        "spellcheck",
+        "src",
+        "srcdoc",
+        "srclang",
+        "srcset",
+        "start",
+        "step",
+        "style",
+        "tabindex",
+        "target",
+        "title",
+        "translate",
+        "type",
+        "usemap",
+        "value",
+        "width",
+        "wrap",
+    ];
+
+    for s in cached_words {
+        wasm_bindgen::intern(s);
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use std::env;

+ 24 - 7
packages/web/src/new.rs

@@ -171,6 +171,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
         tag: &str,
         ns: Option<&'static str>,
     ) -> dioxus_core::virtual_dom::RealDomNode {
+        let tag = wasm_bindgen::intern(tag);
         let el = match ns {
             Some(ns) => self
                 .document
@@ -200,6 +201,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
         el_id: usize,
         real_id: RealDomNode,
     ) {
+        let event = wasm_bindgen::intern(event);
         log::debug!(
             "Called [`new_event_listener`]: {}, {:?}, {}, {:?}",
             event,
@@ -237,7 +239,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
                 // Instead, we just build and immediately execute a closure that returns result
                 match decode_trigger(event) {
                     Ok(synthetic_event) => trigger.as_ref()(synthetic_event),
-                    Err(_) => log::error!("Error decoding Dioxus event attribute."),
+                    Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
                 };
             }) as Box<dyn FnMut(&Event)>);
 
@@ -407,7 +409,7 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
 
             #[derive(Debug)]
             pub struct CustomMouseEvent(web_sys::MouseEvent);
-            impl dioxus_core::events::on::MouseEvent for CustomMouseEvent {
+            impl dioxus_core::events::on::MouseEventInner for CustomMouseEvent {
                 fn alt_key(&self) -> bool {
                     self.0.alt_key()
                 }
@@ -451,7 +453,7 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
                     self.0.get_modifier_state(key_code)
                 }
             }
-            VirtualEvent::MouseEvent(Rc::new(CustomMouseEvent(evt)))
+            VirtualEvent::MouseEvent(MouseEvent(Rc::new(CustomMouseEvent(evt))))
             // MouseEvent(Box::new(RawMouseEvent {
             //                 alt_key: evt.alt_key(),
             //                 button: evt.button() as i32,
@@ -547,25 +549,36 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
 
     use anyhow::Context;
 
+    // for attr in  {
+    let attrs = target.attributes();
+    for x in 0..attrs.length() {
+        let attr = attrs.item(x).unwrap();
+        log::debug!("attrs include: {:#?}", attr);
+    }
+    // }
+    // for attr in target.attributes() {
+    //     log::debug!("attrs include: {:#?}", attr);
+    // }
+
     // The error handling here is not very descriptive and needs to be replaced with a zero-cost error system
     let val: String = target
         .get_attribute(&format!("dioxus-event-{}", typ))
-        .context("")?;
+        .context(format!("wrong format - received {:#?}", typ))?;
 
     let mut fields = val.splitn(3, ".");
 
     let gi_id = fields
         .next()
         .and_then(|f| f.parse::<u64>().ok())
-        .context("")?;
+        .context("failed to parse gi id")?;
     let el_id = fields
         .next()
         .and_then(|f| f.parse::<usize>().ok())
-        .context("")?;
+        .context("failed to parse el id")?;
     let real_id = fields
         .next()
         .and_then(|f| f.parse::<u64>().ok().map(RealDomNode::new))
-        .context("")?;
+        .context("failed to parse real id")?;
 
     // Call the trigger
     log::debug!("decoded gi_id: {}, li_idx: {}", gi_id, el_id);
@@ -575,6 +588,7 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
         virtual_event_from_websys_event(event),
         triggered_scope,
         real_id,
+        dioxus_core::events::EventPriority::High,
     ))
 }
 
@@ -584,3 +598,6 @@ impl ListenerMap {
         false
     }
 }
+
+struct JsStringCache {}
+impl JsStringCache {}