on master help sign in
Support keyed children

Using keyed children gives users more control over whether particular physical nodes are reused, moved, deleted, or created afresh. Specifically, if the old virtual node and the new virtual node have the same key, then the physical DOM node must be reused, and if they don't then the old physical DOM node cannot be reused. Presumably there is some sort of CSS transition animation or third party JS that makes the virtual DOM diffing algorithm observable, or else users would let the runtime do whatever it thought was best. This is loosely based on Inferno's keyed patching implementation. However, we have to modify the algorithm since we are compiling the diff down into change list instructions that will be executed later, rather than applying the changes to the DOM directly as we compare virtual DOMs. https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739

commitad5f96ecea6ea46642ce44108e2bf513681232e4 authorNick Fitzgerald <fitzgen@gmail.com> dateMon 15 Apr 2019, 11:04 (8 years ago) parent3906f80
16 files changed+1249−100
Cargo.lock+8 Cargo.toml+2 js/change-list-interpreter.js+53−7 src/builder.rs+59−2 src/cached.rs+16−13 src/cached_set.rs−3 src/change_list/emitter.rs+56 src/change_list/mod.rs+52−2 src/change_list/strings.rs+4−3 src/diff.rs+625−51 src/events.rs−4 src/node.rs+74−12 src/vdom.rs+2−3 tests/web/keyed.js+25 tests/web/keyed.rs+272 tests/web/main.rs+1
Cargo.lock+8
@@ -205,6 +205,7 @@ dependencies = [
205205name = "dodrio"
206206version = "0.1.0"
207207dependencies = [
208+ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
208209 "bumpalo 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
209210 "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
210211 "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -214,6 +215,7 @@ dependencies = [
214215 "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
215216 "js-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
216217 "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
218+ "longest-increasing-subsequence 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
217219 "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
218220 "wasm-bindgen-futures 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
219221 "wasm-bindgen-test 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -453,6 +455,11 @@ dependencies = [
453455 "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
454456]
455457
458+[[package]]
459+name = "longest-increasing-subsequence"
460+version = "0.1.0"
461+source = "registry+https://github.com/rust-lang/crates.io-index"
462+
456463[[package]]
457464name = "memchr"
458465version = "2.2.0"
@@ -968,6 +975,7 @@ dependencies = [
968975"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
969976"checksum libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "bedcc7a809076656486ffe045abeeac163da1b558e963a31e29fbfbeba916917"
970977"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
978+"checksum longest-increasing-subsequence 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86"
971979"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
972980"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3"
973981"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
Cargo.toml+2
@@ -42,6 +42,8 @@ wasm-bindgen-futures = "0.3.17"
4242js-sys = "0.3.17"
4343fxhash = "0.2.1"
4444log = { version = "0.4.6", optional = true }
45+bitflags = "1.0.4"
46+longest-increasing-subsequence = "0.1.0"
4547
4648[dependencies.web-sys]
4749version = "0.3.17"
js/change-list-interpreter.js+53−7
@@ -188,13 +188,57 @@ const OP_TABLE = [
188188 },
189189
190190 // 17
191− function setAttributeNS(interpreter, mem8, mem32, i) {
192− const nameId = mem32[i++];
193− const valueId = mem32[i++];
194− const name = interpreter.getCachedString(nameId);
195− const value = interpreter.getCachedString(valueId);
196− const node = top(interpreter.stack);
197− node.setAttributeNS(null, name, value);
191+ function saveChildrenToTemporaries(interpreter, mem8, mem32, i) {
192+ let temp = mem32[i++];
193+ const start = mem32[i++];
194+ const end = mem32[i++];
195+ const parent = top(interpreter.stack);
196+ const children = parent.childNodes;
197+ for (let i = start; i < end; i++) {
198+ interpreter.temporaries[temp++] = children[i];
199+ }
200+ return i;
201+ },
202+
203+ // 18
204+ function pushChild(interpreter, mem8, mem32, i) {
205+ const parent = top(interpreter.stack);
206+ const n = mem32[i++];
207+ const child = parent.childNodes[n];
208+ interpreter.stack.push(child);
209+ return i;
210+ },
211+
212+ // 19
213+ function pushTemporary(interpreter, mem8, mem32, i) {
214+ const temp = mem32[i++];
215+ interpreter.stack.push(interpreter.temporaries[temp]);
216+ return i;
217+ },
218+
219+ // 20
220+ function insertBefore(interpreter, mem8, mem32, i) {
221+ const before = interpreter.stack.pop();
222+ const after = interpreter.stack.pop();
223+ after.parentNode.insertBefore(before, after);
224+ interpreter.stack.push(before);
225+ return i;
226+ },
227+
228+ // 21
229+ function pushLastChild(interpreter, mem8, mem32, i) {
230+ const parent = top(interpreter.stack);
231+ const child = parent.lastChild;
232+ interpreter.stack.push(child);
233+ return i;
234+ },
235+
236+ // 22
237+ function removeChild(interpreter, mem8, mem32, i) {
238+ const n = mem32[i++];
239+ const parent = top(interpreter.stack);
240+ const child = parent.childNodes[n];
241+ child.remove();
198242 return i;
199243 }
200244];
@@ -206,6 +250,7 @@ export class ChangeListInterpreter {
206250 this.ranges = [];
207251 this.stack = [];
208252 this.strings = new Map();
253+ this.temporaries = [];
209254 }
210255
211256 unmount() {
@@ -218,6 +263,7 @@ export class ChangeListInterpreter {
218263 this.ranges = null;
219264 this.stack = null;
220265 this.strings = null;
266+ this.temporaries = null;
221267 }
222268
223269 addChangeListRange(start, len) {
src/builder.rs+59−2
@@ -1,6 +1,6 @@
11//! Helpers for building virtual DOM nodes.
22
3−use crate::{Attribute, Listener, Node, RootRender, VdomWeak};
3+use crate::{node::ElementNodeFlags, Attribute, Listener, Node, RootRender, VdomWeak};
44use bumpalo::Bump;
55
66/// A virtual DOM element builder.
@@ -16,6 +16,7 @@ where
1616 Children: 'a + AsRef<[Node<'a>]>,
1717{
1818 bump: &'a Bump,
19+ flags: ElementNodeFlags,
1920 tag_name: &'a str,
2021 listeners: Listeners,
2122 attributes: Attributes,
@@ -61,6 +62,7 @@ impl<'a>
6162 let bump = bump.into();
6263 ElementBuilder {
6364 bump,
65+ flags: Default::default(),
6466 tag_name,
6567 listeners: bumpalo::collections::Vec::new_in(bump),
6668 attributes: bumpalo::collections::Vec::new_in(bump),
@@ -110,6 +112,7 @@ where
110112 {
111113 ElementBuilder {
112114 bump: self.bump,
115+ flags: self.flags,
113116 tag_name: self.tag_name,
114117 listeners,
115118 attributes: self.attributes,
@@ -148,6 +151,7 @@ where
148151 {
149152 ElementBuilder {
150153 bump: self.bump,
154+ flags: self.flags,
151155 tag_name: self.tag_name,
152156 listeners: self.listeners,
153157 attributes,
@@ -186,6 +190,7 @@ where
186190 {
187191 ElementBuilder {
188192 bump: self.bump,
193+ flags: self.flags,
189194 tag_name: self.tag_name,
190195 listeners: self.listeners,
191196 attributes: self.attributes,
@@ -204,7 +209,7 @@ where
204209 /// let b = Bump::new();
205210 ///
206211 /// // Create a `<td>` tag with an xhtml namespace
207− /// let my_div = td(&b)
212+ /// let my_td = td(&b)
208213 /// .namespace(Some("http://www.w3.org/1999/xhtml"))
209214 /// .finish();
210215 /// ```
@@ -212,6 +217,7 @@ where
212217 pub fn namespace(self, namespace: Option<&'a str>) -> Self {
213218 ElementBuilder {
214219 bump: self.bump,
220+ flags: self.flags,
215221 tag_name: self.tag_name,
216222 listeners: self.listeners,
217223 attributes: self.attributes,
@@ -220,6 +226,56 @@ where
220226 }
221227 }
222228
229+ /// Set whether this node has keyed children or not. The default
230+ /// configuration is `false`.
231+ ///
232+ /// # Example
233+ ///
234+ /// ```no_run
235+ /// use dodrio::{builder::*, bumpalo::Bump};
236+ ///
237+ /// let b = Bump::new();
238+ ///
239+ /// // Create a `<ul>` with keyed children.
240+ /// let my_ul = ul(&b)
241+ /// .has_keyed_children(true)
242+ /// .children(create_keyed_children())
243+ /// .finish();
244+ /// # fn create_keyed_children() -> [dodrio::Node<'static>; 0] { unimplemented!() }
245+ /// ```
246+ #[inline]
247+ pub fn has_keyed_children(mut self, has_keyed_children: bool) -> Self {
248+ if has_keyed_children {
249+ self.flags |= ElementNodeFlags::HAS_KEYED_CHILDREN;
250+ } else {
251+ self.flags &= !ElementNodeFlags::HAS_KEYED_CHILDREN;
252+ }
253+ self
254+ }
255+
256+ /// Set this element's key. The key must be less than or equal to `2^31 -
257+ /// 1`.
258+ ///
259+ /// This element's parent must have `has_keyed_children(true)` set.
260+ ///
261+ /// # Example
262+ ///
263+ /// ```no_run
264+ /// use dodrio::{builder::*, bumpalo::Bump};
265+ ///
266+ /// let b = Bump::new();
267+ ///
268+ /// let my_li = li(&b)
269+ /// .key(1337)
270+ /// .finish();
271+ /// ```
272+ #[inline]
273+ pub fn key(mut self, key: u32) -> Self {
274+ debug_assert_eq!(key & (!ElementNodeFlags::KEY_MASK).bits(), 0);
275+ self.flags.set_key(key);
276+ self
277+ }
278+
223279 /// Create the virtual DOM node described by this builder.
224280 ///
225281 /// # Example
@@ -239,6 +295,7 @@ where
239295 pub fn finish(self) -> Node<'a> {
240296 Node::element(
241297 self.bump,
298+ self.flags,
242299 self.tag_name,
243300 self.listeners,
244301 self.attributes,
src/cached.rs+16−13
@@ -1,7 +1,4 @@
1−use crate::{
2− cached_set::{CacheId, CachedSet},
3− Node, Render, RenderContext,
4−};
1+use crate::{cached_set::CachedSet, node::CachedNode, Node, Render, RenderContext};
52use std::cell::Cell;
63use std::ops::{Deref, DerefMut};
74
@@ -10,7 +7,7 @@ use std::ops::{Deref, DerefMut};
107#[derive(Clone, Debug)]
118pub struct Cached<R> {
129 inner: R,
13− cached: Cell<Option<CacheId>>,
10+ cached: Cell<Option<CachedNode>>,
1411}
1512
1613impl<R> Cached<R> {
@@ -112,7 +109,7 @@ where
112109 R: Render,
113110{
114111 fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
115− let id = match self.cached.get() {
112+ let cached = match self.cached.get() {
116113 // This does-the-cache-contain-this-id check is necessary because
117114 // the same `Cached<R>` instance can be rendered into vdom A, which
118115 // will save the results into A's cached set and yield id X. Then,
@@ -140,21 +137,27 @@ where
140137 //
141138 // This is all do-able but is a bit more book keeping than we really
142139 // want to do unless it is well motivated.
143− Some(id)
140+ Some(cached)
144141 if {
145142 let cached_set = cx.cached_set.borrow();
146− cached_set.contains(id)
143+ cached_set.contains(cached.id)
147144 } =>
148145 {
149− id
146+ cached
150147 }
151148 _ => {
152− let id = CachedSet::insert(cx, |nested_cx| self.inner.render(nested_cx));
153− self.cached.set(Some(id));
154− id
149+ let mut flags = Default::default();
150+ let id = CachedSet::insert(cx, |nested_cx| {
151+ let node = self.inner.render(nested_cx);
152+ flags = node.flags();
153+ node
154+ });
155+ let cached = CachedNode { id, flags };
156+ self.cached.set(Some(cached));
157+ cached
155158 }
156159 };
157160
158− Node::cached(id)
161+ cached.into()
159162 }
160163}
src/cached_set.rs−3
@@ -64,7 +64,6 @@ impl CachedSet {
6464 self.items.retain(|id, entry| {
6565 let keep = marked.contains(id);
6666 if !keep {
67− debug!("CachedSet::gc: removing {:?}", id);
6867 let node: &Node = unsafe { &*entry.node };
6968 registry.remove_subtree(node);
7069 }
@@ -129,7 +128,6 @@ impl CachedSet {
129128
130129 let mut set = set.borrow_mut();
131130 let id = set.next_id();
132− debug!("CachedSet::insert: id = {:?}; entry = {:?}", id, entry);
133131 set.items.insert(id, entry);
134132 id
135133 }
@@ -141,7 +139,6 @@ impl CachedSet {
141139
142140 /// Get the node for the given cache id.
143141 pub fn get(&self, mut id: CacheId) -> &Node {
144− debug!("CachedSet::get: id = {:?}", id);
145142 loop {
146143 let entry = self
147144 .items
src/change_list/emitter.rs+56
@@ -201,4 +201,60 @@ define_change_list_instructions! {
201201 /// stack.push(document.createElementNS(tag_name, namespace))
202202 /// ```
203203 create_element_ns(tag_name_key, namespace_key) = 16,
204+
205+ /// Stack: `[...] -> [...]`
206+ ///
207+ /// ```text
208+ /// parent = stack.top()
209+ /// children = parent.childNodes
210+ /// temp = temp_base
211+ /// for i in start .. end:
212+ /// temporaries[temp] = children[i]
213+ /// temp += 1
214+ /// ```
215+ save_children_to_temporaries(temp_base, start, end) = 17,
216+
217+ /// Stack: `[... Node] -> [... Node Node]`
218+ ///
219+ /// ```text
220+ /// parent = stack.top()
221+ /// child = parent.childNodes[n]
222+ /// stack.push(child)
223+ /// ```
224+ push_child(n) = 18,
225+
226+ /// Stack: `[...] -> [... Node]`
227+ ///
228+ /// ```text
229+ /// stack.push(temporaries[temp])
230+ /// ```
231+ push_temporary(temp) = 19,
232+
233+ /// Stack: `[... Node Node] -> [... Node]`
234+ ///
235+ /// ```text
236+ /// before = stack.pop()
237+ /// after = stack.pop()
238+ /// after.insertBefore(before)
239+ /// stack.push(before)
240+ /// ```
241+ insert_before() = 20,
242+
243+ /// Stack: `[... Node] -> [... Node Node]`
244+ ///
245+ /// ```text
246+ /// parent = stack.top()
247+ /// child = parent.lastChild
248+ /// stack.push(child)
249+ /// ```
250+ push_last_child() = 21,
251+
252+ /// Stack: `[... Node] -> [... Node]`
253+ ///
254+ /// ```text
255+ /// parent = stack.top()
256+ /// child = parent.childNodes[n]
257+ /// child.remove()
258+ /// ```
259+ remove_child(n) = 22,
204260}
src/change_list/mod.rs+52−2
@@ -17,11 +17,11 @@ pub(crate) struct ChangeListPersistentState {
1717
1818pub(crate) struct ChangeListBuilder<'a> {
1919 state: &'a mut ChangeListPersistentState,
20+ next_temporary: u32,
2021}
2122
2223impl Drop for ChangeListPersistentState {
2324 fn drop(&mut self) {
24− debug!("Dropping ChangeListPersistentState");
2525 self.interpreter.unmount();
2626 }
2727}
@@ -43,7 +43,10 @@ impl ChangeListPersistentState {
4343 }
4444
4545 pub(crate) fn builder<'a>(&'a mut self) -> ChangeListBuilder<'a> {
46− ChangeListBuilder { state: self }
46+ ChangeListBuilder {
47+ state: self,
48+ next_temporary: 0,
49+ }
4750 }
4851}
4952
@@ -77,6 +80,53 @@ cfg_if::cfg_if! {
7780}
7881
7982impl ChangeListBuilder<'_> {
83+ pub fn next_temporary(&self) -> u32 {
84+ self.next_temporary
85+ }
86+
87+ pub fn set_next_temporary(&mut self, next_temporary: u32) {
88+ self.next_temporary = next_temporary;
89+ }
90+
91+ pub fn save_children_to_temporaries(&mut self, start: usize, end: usize) -> u32 {
92+ debug_assert!(start < end);
93+ let temp_base = self.next_temporary;
94+ debug!(
95+ "emit: save_children_to_temporaries({}, {}, {})",
96+ temp_base, start, end
97+ );
98+ self.next_temporary = temp_base + (end - start) as u32;
99+ self.state
100+ .emitter
101+ .save_children_to_temporaries(temp_base, start as u32, end as u32);
102+ temp_base
103+ }
104+
105+ pub fn push_temporary(&self, temp: u32) {
106+ debug!("emit: push_temporary({})", temp);
107+ self.state.emitter.push_temporary(temp);
108+ }
109+
110+ pub fn push_child(&self, child: usize) {
111+ debug!("emit: push_child({})", child);
112+ self.state.emitter.push_child(child as u32);
113+ }
114+
115+ pub fn remove_child(&self, child: usize) {
116+ debug!("emit: remove_child({})", child);
117+ self.state.emitter.remove_child(child as u32);
118+ }
119+
120+ pub fn push_last_child(&self) {
121+ debug!("emit: push_last_child()");
122+ self.state.emitter.push_last_child();
123+ }
124+
125+ pub fn insert_before(&self) {
126+ debug!("emit: insert_before()");
127+ self.state.emitter.insert_before();
128+ }
129+
80130 pub fn ensure_string(&mut self, string: &str) -> StringKey {
81131 self.state
82132 .strings
src/change_list/strings.rs+4−3
@@ -45,8 +45,7 @@ impl StringsCache {
4545 }
4646
4747 pub fn drop_unused_strings(&mut self, emitter: &InstructionEmitter) {
48− debug!("drop_unused_strings()");
49− self.entries.retain(|_string, entry| {
48+ self.entries.retain(|string, entry| {
5049 if entry.used {
5150 // Since this entry was used during while rendering this frame,
5251 // it is likely going to be used for the next frame as well. So
@@ -57,7 +56,9 @@ impl StringsCache {
5756 entry.used = false;
5857 true
5958 } else {
60− emitter.drop_cached_string(entry.key.into());
59+ let key = entry.key.into();
60+ debug!("emit: drop_cached_string({}) = {:?}", key, string);
61+ emitter.drop_cached_string(key);
6162 false
6263 }
6364 });
src/diff.rs+625−51
@@ -2,11 +2,22 @@ use crate::{
22 cached_set::{CacheId, CachedSet},
33 change_list::ChangeListBuilder,
44 events::EventsRegistry,
5− node::{Attribute, ElementNode, Listener, Node, NodeKind, TextNode},
5+ node::{Attribute, ElementNode, ElementNodeFlags, Listener, Node, NodeKind, TextNode},
66};
7−use fxhash::FxHashSet;
8−use std::cmp;
7+use fxhash::{FxHashMap, FxHashSet};
8+use std::cmp::Ordering;
9+use std::ops::Range;
10+use std::u32;
911
12+// Diff the `old` node with the `new` node. Emits instructions to modify a
13+// physical DOM node that reflects `old` into something that reflects `new`.
14+//
15+// Upon entry to this function, the physical DOM node must be on the top of the
16+// stack:
17+//
18+// [... node]
19+//
20+// The stack is in the same state when this function exits.
1021pub(crate) fn diff(
1122 cached_set: &CachedSet,
1223 change_list: &mut ChangeListBuilder,
@@ -20,22 +31,18 @@ pub(crate) fn diff(
2031 &NodeKind::Text(TextNode { text: new_text }),
2132 &NodeKind::Text(TextNode { text: old_text }),
2233 ) => {
23− debug!(" both are text nodes");
2434 if new_text != old_text {
25− debug!(" text needs updating");
2635 change_list.set_text(new_text);
2736 }
2837 }
2938
3039 (&NodeKind::Text(_), &NodeKind::Element(_)) => {
31− debug!(" replacing a text node with an element");
3240 create(cached_set, change_list, registry, new, cached_roots);
3341 registry.remove_subtree(&old);
3442 change_list.replace_with();
3543 }
3644
3745 (&NodeKind::Element(_), &NodeKind::Text(_)) => {
38− debug!(" replacing an element with a text node");
3946 create(cached_set, change_list, registry, new, cached_roots);
4047 // Note: text nodes cannot have event listeners, so we don't need to
4148 // remove the old node's listeners from our registry her.
@@ -44,6 +51,7 @@ pub(crate) fn diff(
4451
4552 (
4653 &NodeKind::Element(ElementNode {
54+ flags: new_flags,
4755 tag_name: new_tag_name,
4856 listeners: new_listeners,
4957 attributes: new_attributes,
@@ -51,6 +59,7 @@ pub(crate) fn diff(
5159 namespace: new_namespace,
5260 }),
5361 &NodeKind::Element(ElementNode {
62+ flags: old_flags,
5463 tag_name: old_tag_name,
5564 listeners: old_listeners,
5665 attributes: old_attributes,
@@ -58,9 +67,7 @@ pub(crate) fn diff(
5867 namespace: old_namespace,
5968 }),
6069 ) => {
61− debug!(" updating an element");
6270 if new_tag_name != old_tag_name || new_namespace != old_namespace {
63− debug!(" different tag names or namespaces; creating new element and replacing old element");
6471 create(cached_set, change_list, registry, new, cached_roots);
6572 registry.remove_subtree(&old);
6673 change_list.replace_with();
@@ -72,6 +79,8 @@ pub(crate) fn diff(
7279 cached_set,
7380 change_list,
7481 registry,
82+ new_flags,
83+ old_flags,
7584 old_children,
7685 new_children,
7786 cached_roots,
@@ -113,14 +122,19 @@ pub(crate) fn diff(
113122 }
114123}
115124
125+// Diff event listeners between `old` and `new`.
126+//
127+// The listeners' node must be on top of the stack:
128+//
129+// [... node]
130+//
131+// The stack is left unchanged.
116132fn diff_listeners(
117133 change_list: &mut ChangeListBuilder,
118134 registry: &mut EventsRegistry,
119135 old: &[Listener],
120136 new: &[Listener],
121137) {
122− debug!(" updating event listeners");
123−
124138 'outer1: for new_l in new {
125139 unsafe {
126140 // Safety relies on removing `new_l` from the registry manually at
@@ -151,9 +165,14 @@ fn diff_listeners(
151165 }
152166}
153167
168+// Diff a node's attributes.
169+//
170+// The attributes' node must be on top of the stack:
171+//
172+// [... node]
173+//
174+// The stack is left unchanged.
154175fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new: &[Attribute]) {
155− debug!(" updating attributes");
156−
157176 // Do O(n^2) passes to add/update and remove attributes, since
158177 // there are almost always very few attributes.
159178 'outer: for new_attr in new {
@@ -182,104 +201,629 @@ fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new:
182201 }
183202}
184203
204+// Diff the given set of old and new children.
205+//
206+// The parent must be on top of the stack when this function is entered:
207+//
208+// [... parent]
209+//
210+// The stack is in the same state when this function returns.
185211fn diff_children(
186212 cached_set: &CachedSet,
187213 change_list: &mut ChangeListBuilder,
188214 registry: &mut EventsRegistry,
215+ new_flags: ElementNodeFlags,
216+ old_flags: ElementNodeFlags,
189217 old: &[Node],
190218 new: &[Node],
191219 cached_roots: &mut FxHashSet<CacheId>,
192220) {
193221 if new.is_empty() {
194222 if !old.is_empty() {
195− remove_children(change_list, registry, old);
223+ remove_all_children(change_list, registry, old);
196224 }
197225 return;
198226 }
199227
200228 if old.is_empty() {
201− create_children(cached_set, change_list, registry, new, cached_roots);
229+ create_and_append_children(cached_set, change_list, registry, new, cached_roots);
202230 return;
203231 }
204232
205− debug!(" updating children shared by old and new");
233+ if new_flags.has_keyed_children() && old_flags.has_keyed_children() {
234+ let t = change_list.next_temporary();
235+ diff_keyed_children(cached_set, change_list, registry, old, new, cached_roots);
236+ change_list.set_next_temporary(t);
237+ } else {
238+ diff_non_keyed_children(cached_set, change_list, registry, old, new, cached_roots);
239+ }
240+}
241+
242+// Diffing "keyed" children.
243+//
244+// With keyed children, we care about whether we delete, move, or create nodes
245+// versus mutate existing nodes in place. Presumably there is some sort of CSS
246+// transition animation that makes the virtual DOM diffing algorithm
247+// observable. By specifying keys for nodes, we know which virtual DOM nodes
248+// must reuse (or not reuse) the same physical DOM nodes.
249+//
250+// This is loosely based on Inferno's keyed patching implementation. However, we
251+// have to modify the algorithm since we are compiling the diff down into change
252+// list instructions that will be executed later, rather than applying the
253+// changes to the DOM directly as we compare virtual DOMs.
254+//
255+// https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739
256+//
257+// When entering this function, the parent must be on top of the stack:
258+//
259+// [... parent]
260+//
261+// Upon exiting, the stack is in the same state.
262+fn diff_keyed_children(
263+ cached_set: &CachedSet,
264+ change_list: &mut ChangeListBuilder,
265+ registry: &mut EventsRegistry,
266+ old: &[Node],
267+ new: &[Node],
268+ cached_roots: &mut FxHashSet<CacheId>,
269+) {
270+ // First up, we diff all the nodes with the same key at the beginning of the
271+ // children.
272+ //
273+ // `shared_prefix_count` is the count of how many nodes at the start of
274+ // `new` and `old` share the same keys.
275+ let shared_prefix_count =
276+ match diff_keyed_prefix(cached_set, change_list, registry, old, new, cached_roots) {
277+ KeyedPrefixResult::Finished => return,
278+ KeyedPrefixResult::MoreWorkToDo(count) => count,
279+ };
280+
281+ // Next, we find out how many of the nodes at the end of the children have
282+ // the same key. We do _not_ diff them yet, since we want to emit the change
283+ // list instructions such that they can be applied in a single pass over the
284+ // DOM. Instead, we just save this information for later.
285+ //
286+ // `shared_suffix_count` is the count of how many nodes at the end of `new`
287+ // and `old` share the same keys.
288+ let shared_suffix_count = old[shared_prefix_count..]
289+ .iter()
290+ .rev()
291+ .zip(new[shared_prefix_count..].iter().rev())
292+ .take_while(|&(old, new)| old.key() == new.key())
293+ .count();
294+
295+ let old_shared_suffix_start = old.len() - shared_suffix_count;
296+ let new_shared_suffix_start = new.len() - shared_suffix_count;
297+
298+ // Ok, we now hopefully have a smaller range of children in the middle
299+ // within which to re-order nodes with the same keys, remove old nodes with
300+ // now-unused keys, and create new nodes with fresh keys.
301+ diff_keyed_middle(
302+ cached_set,
303+ change_list,
304+ registry,
305+ &old[shared_prefix_count..old_shared_suffix_start],
306+ &new[shared_prefix_count..new_shared_suffix_start],
307+ cached_roots,
308+ shared_prefix_count,
309+ shared_suffix_count,
310+ old_shared_suffix_start,
311+ );
312+
313+ // Finally, diff the nodes at the end of `old` and `new` that share keys.
314+ let old_suffix = &old[old_shared_suffix_start..];
315+ let new_suffix = &new[new_shared_suffix_start..];
316+ debug_assert_eq!(old_suffix.len(), new_suffix.len());
317+ if !old_suffix.is_empty() {
318+ diff_keyed_suffix(
319+ cached_set,
320+ change_list,
321+ registry,
322+ old_suffix,
323+ new_suffix,
324+ cached_roots,
325+ new_shared_suffix_start,
326+ );
327+ }
328+}
329+
330+enum KeyedPrefixResult {
331+ // Fast path: we finished diffing all the children just by looking at the
332+ // prefix of shared keys!
333+ Finished,
334+ // There is more diffing work to do. Here is a count of how many children at
335+ // the beginning of `new` and `old` we already processed.
336+ MoreWorkToDo(usize),
337+}
206338
207− let num_children_to_diff = cmp::min(new.len(), old.len());
208− let mut new_children = new.iter();
209− let mut old_children = old.iter();
339+// Diff the prefix of children in `new` and `old` that share the same keys in
340+// the same order.
341+//
342+// Upon entry of this function, the stack must be:
343+//
344+// [... parent]
345+//
346+// Upon exit, the stack is the same.
347+fn diff_keyed_prefix(
348+ cached_set: &CachedSet,
349+ change_list: &mut ChangeListBuilder,
350+ registry: &mut EventsRegistry,
351+ old: &[Node],
352+ new: &[Node],
353+ cached_roots: &mut FxHashSet<CacheId>,
354+) -> KeyedPrefixResult {
210355 let mut pushed = false;
356+ let mut shared_prefix_count = 0;
211357
212− for (i, (new_child, old_child)) in new_children
213− .by_ref()
214− .zip(old_children.by_ref())
215− .take(num_children_to_diff)
216− .enumerate()
217− {
218− if i == 0 {
358+ for (old, new) in old.iter().zip(new.iter()) {
359+ if old.key() != new.key() {
360+ break;
361+ }
362+
363+ if pushed {
364+ debug_assert!(shared_prefix_count > 0);
365+ change_list.pop_push_next_sibling();
366+ } else {
367+ debug_assert_eq!(shared_prefix_count, 0);
219368 change_list.push_first_child();
220369 pushed = true;
221− } else {
222− debug_assert!(pushed);
223− change_list.pop_push_next_sibling();
224370 }
225371
226− diff(
372+ diff(cached_set, change_list, registry, old, new, cached_roots);
373+ shared_prefix_count += 1;
374+
375+ // At the end of the loop, the stack looks like
376+ //
377+ // [... parent child_we_just_diffed]
378+ debug_assert!(pushed);
379+ }
380+
381+ // If that was all of the old children, then create and append the remaining
382+ // new children and we're finished.
383+ if shared_prefix_count == old.len() {
384+ debug_assert!(
385+ pushed,
386+ "we handle the case of empty children before calling this method, so
387+ `shared_prefix_count` must be greater than zero, and therefore we
388+ must have pushed in the above loop"
389+ );
390+ change_list.pop();
391+ create_and_append_children(
227392 cached_set,
228393 change_list,
229394 registry,
230− old_child,
231− new_child,
395+ &new[shared_prefix_count..],
232396 cached_roots,
233397 );
398+ return KeyedPrefixResult::Finished;
234399 }
235400
236− if old_children.next().is_some() {
237− debug!(" removing extra old children");
238− debug_assert!(new_children.next().is_none());
239− if pushed {
401+ // And if that was all of the new children, then remove all of the remaining
402+ // old children and we're finished.
403+ if shared_prefix_count == new.len() {
404+ // Same as above.
405+ debug_assert!(pushed);
406+ change_list.pop_push_next_sibling();
407+ change_list.remove_self_and_next_siblings();
408+ return KeyedPrefixResult::Finished;
409+ }
410+
411+ if pushed {
412+ debug_assert!(shared_prefix_count > 0);
413+ change_list.pop();
414+ }
415+ KeyedPrefixResult::MoreWorkToDo(shared_prefix_count)
416+}
417+
418+// The most-general, expensive code path for keyed children diffing.
419+//
420+// We find the longest subsequence within `old` of children that are relatively
421+// ordered the same way in `new` (via finding a longest-increasing-subsequence
422+// of the old child's index within `new`). The children that are elements of
423+// this subsequence will remain in place, minimizing the number of DOM moves we
424+// will have to do.
425+//
426+// Upon entry to this function, the stack must be:
427+//
428+// [... parent]
429+//
430+// Upon exit from this function, it will be restored to that same state.
431+fn diff_keyed_middle(
432+ cached_set: &CachedSet,
433+ change_list: &mut ChangeListBuilder,
434+ registry: &mut EventsRegistry,
435+ old: &[Node],
436+ new: &[Node],
437+ cached_roots: &mut FxHashSet<CacheId>,
438+ shared_prefix_count: usize,
439+ shared_suffix_count: usize,
440+ old_shared_suffix_start: usize,
441+) {
442+ // Should have already diffed the shared-key prefixes and suffixes.
443+ debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key()));
444+ debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key()));
445+
446+ // The algorithm below relies upon using `u32::MAX` as a sentinel
447+ // value, so if we have that many new nodes, it won't work. This
448+ // check is a bit academic (hence only enabled in debug), since
449+ // wasm32 doesn't have enough address space to hold that many nodes
450+ // in memory.
451+ debug_assert!(new.len() < u32::MAX as usize);
452+
453+ // Map from each `old` node's key to its index within `old`.
454+ let mut old_key_to_old_index = FxHashMap::default();
455+ old_key_to_old_index.reserve(old.len());
456+ old_key_to_old_index.extend(old.iter().enumerate().map(|(i, o)| (o.key(), i)));
457+
458+ // The set of shared keys between `new` and `old`.
459+ let mut shared_keys = FxHashSet::default();
460+ // Map from each index in `new` to the index of the node in `old` that
461+ // has the same key.
462+ let mut new_index_to_old_index = Vec::with_capacity(new.len());
463+ new_index_to_old_index.extend(new.iter().map(|n| {
464+ let key = n.key();
465+ if let Some(&i) = old_key_to_old_index.get(&key) {
466+ shared_keys.insert(key);
467+ i
468+ } else {
469+ u32::MAX as usize
470+ }
471+ }));
472+
473+ // If none of the old keys are reused by the new children, then we
474+ // remove all the remaining old children and create the new children
475+ // afresh.
476+ if shared_suffix_count == 0 && shared_keys.is_empty() {
477+ if shared_prefix_count == 0 {
478+ remove_all_children(change_list, registry, old);
479+ } else {
240480 change_list.pop_push_next_sibling();
481+ remove_self_and_next_siblings(change_list, registry, &old[shared_prefix_count..]);
482+ }
483+ create_and_append_children(cached_set, change_list, registry, new, cached_roots);
484+ return;
485+ }
486+
487+ // The longest increasing subsequence within `new_index_to_old_index` in
488+ // reverse order (since `lis_with` adds the indices in reverse
489+ // order). We will leave these nodes in place in the DOM, and only move
490+ // nodes that are not part of the LIS. This results in the minimum
491+ // number of DOM nodes moved.
492+ let mut reverse_lis = Vec::with_capacity(new_index_to_old_index.len());
493+ let mut predecessors = vec![0; new_index_to_old_index.len()];
494+ let mut starts = vec![0; new_index_to_old_index.len()];
495+ longest_increasing_subsequence::lis_with(
496+ &new_index_to_old_index,
497+ &mut reverse_lis,
498+ |a, b| a < b,
499+ &mut predecessors,
500+ &mut starts,
501+ );
502+
503+ // Save each of the old children whose keys are reused in the new
504+ // children.
505+ let mut old_index_to_temp = vec![u32::MAX; old.len()];
506+ let mut start = 0;
507+ loop {
508+ let end = (start..old.len())
509+ .find(|&i| {
510+ let key = old[i].key();
511+ !shared_keys.contains(&key)
512+ })
513+ .unwrap_or(old.len());
514+
515+ if end - start > 0 {
516+ let mut t = change_list.save_children_to_temporaries(
517+ shared_prefix_count + start,
518+ shared_prefix_count + end,
519+ );
520+ for i in start..end {
521+ old_index_to_temp[i] = t;
522+ t += 1;
523+ }
524+ }
525+
526+ debug_assert!(end <= old.len());
527+ if end == old.len() {
528+ break;
241529 } else {
242− change_list.push_first_child();
530+ start = end + 1;
243531 }
244− change_list.remove_self_and_next_siblings();
245− pushed = false;
246− } else {
247− debug!(" creating new children");
532+ }
533+
534+ // Remove any old children whose keys were not reused in the new
535+ // children. Remove from the end first so that we don't mess up indices.
536+ let mut removed_count = 0;
537+ for (i, old_child) in old.iter().enumerate().rev() {
538+ if !shared_keys.contains(&old_child.key()) {
539+ change_list.remove_child(i + shared_prefix_count);
540+ removed_count += 1;
541+ }
542+ }
543+
544+ // Whether we have pushed a child onto the stack or not.
545+ let mut pushed = false;
546+
547+ // Now iterate from the end of the new children back to the beginning,
548+ // moving old children to their new destination, diffing them, and
549+ // creating new children as necessary. Note that iterating in reverse
550+ // order lets us use `Node.prototype.insertBefore` to move/insert
551+ // children.
552+ if shared_suffix_count > 0 {
553+ change_list.push_child(old_shared_suffix_start - removed_count);
554+ pushed = true;
555+ }
556+ let mut segment_end = new.len();
557+ for lis_index in reverse_lis {
558+ // Because we use `u32::MAX` as a sentinel value representing "a child
559+ // with this key is not a member of `new`", we filter that out here.
560+ let old_index = new_index_to_old_index[lis_index];
561+ if old_index == u32::MAX as usize {
562+ continue;
563+ }
564+
565+ // A segment begins with a member of the LIS, which will remain in
566+ // place in the DOM, followed by zero or more new children that are
567+ // not members of the LIS. If one of these new children shares a key
568+ // with one of the old children, then we will move that old child in
569+ // the DOM. Otherwise, we create the new child afresh.
570+ debug_assert!(segment_end - lis_index > 0);
571+
572+ // Go through each of the non-LIS child and move-and-diff or create
573+ // it.
574+ diff_and_move_or_create_segment(
575+ cached_set,
576+ change_list,
577+ registry,
578+ old,
579+ new,
580+ cached_roots,
581+ lis_index + 1..segment_end,
582+ &new_index_to_old_index,
583+ &old_index_to_temp,
584+ &mut pushed,
585+ );
586+
587+ // Now we push the member of the LIS and diff it with its
588+ // correspondingly-keyed new child.
589+ let new_child = &new[lis_index];
590+ let temp = old_index_to_temp[old_index];
591+ debug_assert_ne!(temp, u32::MAX);
248592 if pushed {
249593 change_list.pop();
250− pushed = false;
251594 }
252− create_children(
595+ change_list.push_temporary(temp);
596+ pushed = true;
597+ diff(
253598 cached_set,
254599 change_list,
255600 registry,
256− new_children,
601+ &old[old_index],
602+ new_child,
257603 cached_roots,
258604 );
605+
606+ segment_end = lis_index;
259607 }
260608
261− debug!(" done updating children");
609+ // And now diff-and-move or create each of the non-LIS children that
610+ // appear before the first LIS-member.
611+ diff_and_move_or_create_segment(
612+ cached_set,
613+ change_list,
614+ registry,
615+ old,
616+ new,
617+ cached_roots,
618+ 0..segment_end,
619+ &new_index_to_old_index,
620+ &old_index_to_temp,
621+ &mut pushed,
622+ );
623+
262624 if pushed {
263625 change_list.pop();
264626 }
265627}
266628
267−fn create_children<'a, I>(
629+// Diff-and-then-move or create each new node in the given segment. The
630+// segment's nodes must _not_ be members of the LIS (those nodes we will diff in
631+// place elsewhere, and we should not move them nor create them afresh).
632+//
633+// On entering this function, the stack has the parent and optionally the next
634+// sibling after this segment of children:
635+//
636+// [... parent next_sibling] if *pushed == true
637+// [... parent] otherwise
638+//
639+// After exiting, if any children were moved or created, then `pushed` is set to
640+// true and the stack has the first child in this segment on top. Otherwise the
641+// stack is left as it was on entering.
642+//
643+// [... parent child] if *pushed == true
644+// [... parent] otherwise
645+fn diff_and_move_or_create_segment(
646+ cached_set: &CachedSet,
647+ change_list: &mut ChangeListBuilder,
648+ registry: &mut EventsRegistry,
649+ old: &[Node],
650+ new: &[Node],
651+ cached_roots: &mut FxHashSet<CacheId>,
652+ segment: Range<usize>,
653+ new_index_to_old_index: &[usize],
654+ old_index_to_temp: &[u32],
655+ pushed: &mut bool,
656+) {
657+ for new_index in segment.rev() {
658+ let new_child = &new[new_index];
659+ let old_index = new_index_to_old_index[new_index];
660+ if old_index == u32::MAX as usize {
661+ // The key is not reused. Create this new child afresh.
662+ create(cached_set, change_list, registry, new_child, cached_roots);
663+ } else {
664+ // The key is reused. Diff it with the old node and then
665+ // move the old node into its final destination.
666+ let temp = old_index_to_temp[old_index];
667+ debug_assert_ne!(temp, u32::MAX);
668+ change_list.push_temporary(temp);
669+ diff(
670+ cached_set,
671+ change_list,
672+ registry,
673+ new_child,
674+ &old[old_index],
675+ cached_roots,
676+ );
677+ }
678+
679+ // At this point the stack can have one of two shapes.
680+ if *pushed {
681+ // [... parent next_child new_child]
682+ change_list.insert_before();
683+ // [... parent new_child]
684+ } else {
685+ // [... parent new_child]
686+ change_list.append_child();
687+ // [... parent]
688+ change_list.push_last_child();
689+ // [... parent new_child]
690+ *pushed = true;
691+ }
692+
693+ // At the end of each loop iteration, the stack looks like:
694+ //
695+ // [... parent new_child]
696+ debug_assert!(*pushed);
697+ }
698+}
699+
700+// Diff the suffix of keyed children that share the same keys in the same order.
701+//
702+// The parent must be on the stack when we enter this function:
703+//
704+// [... parent]
705+//
706+// When this function exits, the stack remains the same.
707+fn diff_keyed_suffix(
708+ cached_set: &CachedSet,
709+ change_list: &mut ChangeListBuilder,
710+ registry: &mut EventsRegistry,
711+ old: &[Node],
712+ new: &[Node],
713+ cached_roots: &mut FxHashSet<CacheId>,
714+ new_shared_suffix_start: usize,
715+) {
716+ debug_assert_eq!(old.len(), new.len());
717+ debug_assert!(!old.is_empty());
718+
719+ // [... parent]
720+ change_list.push_child(new_shared_suffix_start);
721+ // [... parent new_child]
722+
723+ for (old_child, new_child) in old.iter().zip(new.iter()) {
724+ diff(
725+ cached_set,
726+ change_list,
727+ registry,
728+ old_child,
729+ new_child,
730+ cached_roots,
731+ );
732+
733+ // [... parent this_new_child]
734+ change_list.pop_push_next_sibling();
735+ // [... parent next_new_child]
736+ }
737+
738+ // [... parent]
739+ change_list.pop();
740+}
741+
742+// Diff children that are not keyed.
743+//
744+// The parent must be on the top of the stack when entering this function:
745+//
746+// [... parent]
747+//
748+// The stack is in the same state when this function returns.
749+fn diff_non_keyed_children(
750+ cached_set: &CachedSet,
751+ change_list: &mut ChangeListBuilder,
752+ registry: &mut EventsRegistry,
753+ old: &[Node],
754+ new: &[Node],
755+ cached_roots: &mut FxHashSet<CacheId>,
756+) {
757+ // Handled these cases in `diff_children` before calling this function.
758+ debug_assert!(!new.is_empty());
759+ debug_assert!(!old.is_empty());
760+
761+ for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() {
762+ if i == 0 {
763+ change_list.push_first_child();
764+ } else {
765+ change_list.pop_push_next_sibling();
766+ }
767+
768+ diff(
769+ cached_set,
770+ change_list,
771+ registry,
772+ old_child,
773+ new_child,
774+ cached_roots,
775+ );
776+ }
777+
778+ match old.len().cmp(&new.len()) {
779+ Ordering::Greater => {
780+ change_list.pop_push_next_sibling();
781+ remove_self_and_next_siblings(change_list, registry, &old[new.len()..]);
782+ }
783+ Ordering::Less => {
784+ change_list.pop();
785+ create_and_append_children(
786+ cached_set,
787+ change_list,
788+ registry,
789+ &new[old.len()..],
790+ cached_roots,
791+ );
792+ }
793+ Ordering::Equal => {
794+ change_list.pop();
795+ }
796+ }
797+}
798+
799+// Create the given children and append them to the parent node.
800+//
801+// The parent node must currently be on top of the stack:
802+//
803+// [... parent]
804+//
805+// When this function returns, the stack is in the same state.
806+fn create_and_append_children(
268807 cached_set: &CachedSet,
269808 change_list: &mut ChangeListBuilder,
270809 registry: &mut EventsRegistry,
271− new: I,
810+ new: &[Node],
272811 cached_roots: &mut FxHashSet<CacheId>,
273−) where
274− I: IntoIterator<Item = &'a Node<'a>>,
275−{
812+) {
276813 for child in new {
277814 create(cached_set, change_list, registry, child, cached_roots);
278815 change_list.append_child();
279816 }
280817}
281818
282−fn remove_children(
819+// Remove all of a node's children.
820+//
821+// The stack must have this shape upon entry to this function:
822+//
823+// [... parent]
824+//
825+// When this function returns, the stack is in the same state.
826+fn remove_all_children(
283827 change_list: &mut ChangeListBuilder,
284828 registry: &mut EventsRegistry,
285829 old: &[Node],
@@ -292,6 +836,35 @@ fn remove_children(
292836 change_list.set_text("");
293837}
294838
839+// Remove the current child and all of its following siblings.
840+//
841+// The stack must have this shape upon entry to this function:
842+//
843+// [... parent child]
844+//
845+// After the function returns, the child is no longer on the stack:
846+//
847+// [... parent]
848+fn remove_self_and_next_siblings(
849+ change_list: &mut ChangeListBuilder,
850+ registry: &mut EventsRegistry,
851+ old: &[Node],
852+) {
853+ for child in old {
854+ registry.remove_subtree(child);
855+ }
856+ change_list.remove_self_and_next_siblings();
857+}
858+
859+// Emit instructions to create the given virtual node.
860+//
861+// The stack may have any shape upon entering this function:
862+//
863+// [...]
864+//
865+// When this function returns, the new node is on top of the stack:
866+//
867+// [... node]
295868fn create(
296869 cached_set: &CachedSet,
297870 change_list: &mut ChangeListBuilder,
@@ -304,6 +877,7 @@ fn create(
304877 change_list.create_text_node(text);
305878 }
306879 NodeKind::Element(ElementNode {
880+ flags: _,
307881 tag_name,
308882 listeners,
309883 attributes,
src/events.rs−4
@@ -64,7 +64,6 @@ cfg_if::cfg_if! {
6464
6565 let weak_registry = Rc::downgrade(&registry);
6666 let closure = Closure::wrap(Box::new(move |event, a, b| {
67− debug!("Events trampoline invoked with (0x{:x}, 0x{:x})", a, b);
6867 debug_assert!(a != 0);
6968
7069 // if the VdomInnerExclusive is keeping this closure alive, then the
@@ -95,7 +94,6 @@ cfg_if::cfg_if! {
9594
9695 pub(crate) fn remove(&mut self, listener: &Listener) {
9796 let id = listener.get_callback_parts();
98− debug!("EventsRegistry::remove(0x{:x}, 0x{:x})", id.0, id.1);
9997 debug_assert!(id.0 != 0);
10098 self.active.remove(&id);
10199 }
@@ -128,7 +126,6 @@ cfg_if::cfg_if! {
128126 /// diffing.
129127 pub(crate) unsafe fn add<'a>(&mut self, listener: &'a Listener<'a>) {
130128 let id = listener.get_callback_parts();
131− debug!("EventsRegistry::add(0x{:x}, 0x{:x})", id.0, id.1);
132129 debug_assert!(id.0 != 0);
133130
134131 let callback =
@@ -139,7 +136,6 @@ cfg_if::cfg_if! {
139136
140137 /// Clear all event listeners from the registry.
141138 pub(crate) fn clear_active_listeners(&mut self) {
142− debug!("EventsRegistry::clear_active_listeners()");
143139 self.active.clear();
144140 }
145141 }
src/node.rs+74−12
@@ -2,8 +2,8 @@ use crate::{cached_set::CacheId, RootRender, VdomWeak};
22use bumpalo::Bump;
33use std::fmt;
44use std::iter;
5−use std::marker::PhantomData;
65use std::mem;
6+use std::u32;
77
88/// A virtual DOM node.
99#[derive(Debug, Clone)]
@@ -28,7 +28,7 @@ pub_unstable_internal! {
2828
2929 /// A node in the vdom's `CachedSet`. This allows us to avoid
3030 /// re-rendering and re-diffing subtrees.
31− Cached(CachedNode<'a>),
31+ Cached(CachedNode),
3232 }
3333}
3434
@@ -41,11 +41,49 @@ pub_unstable_internal! {
4141 }
4242}
4343
44+mod flags {
45+ bitflags::bitflags! {
46+ #[derive(Default)]
47+ pub struct ElementNodeFlags: u32 {
48+ const HAS_KEYED_CHILDREN = 1 << 31;
49+ const KEY_MASK = !(Self::HAS_KEYED_CHILDREN).bits;
50+ }
51+ }
52+
53+ impl ElementNodeFlags {
54+ #[inline]
55+ pub fn has_keyed_children(self) -> bool {
56+ self.contains(ElementNodeFlags::HAS_KEYED_CHILDREN)
57+ }
58+
59+ #[inline]
60+ pub fn key(self) -> u32 {
61+ (self & ElementNodeFlags::KEY_MASK).bits
62+ }
63+
64+ #[inline]
65+ pub fn set_key(&mut self, key: u32) {
66+ debug_assert_eq!(key & (!ElementNodeFlags::KEY_MASK).bits, 0);
67+ self.bits |= key;
68+ }
69+ }
70+}
71+
72+cfg_if::cfg_if! {
73+ if #[cfg(feature = "xxx-unstable-internal-use-only")] {
74+ #[doc(hidden)]
75+ pub use self::flags::ElementNodeFlags;
76+ } else {
77+ pub(crate) use self::flags::ElementNodeFlags;
78+ }
79+}
80+
4481pub_unstable_internal! {
4582 /// Elements have a tag name, zero or more attributes, and zero or more
4683 /// children.
4784 #[derive(Debug, Clone)]
4885 pub(crate) struct ElementNode<'a> {
86+ pub flags: ElementNodeFlags,
4987 pub tag_name: &'a str,
5088 pub listeners: &'a [Listener<'a>],
5189 pub attributes: &'a [Attribute<'a>],
@@ -59,13 +97,19 @@ pub_unstable_internal! {
5997 /// allows us to avoid both re-rendering a sub-tree and re-diffing
6098 /// it. `CachedNode`s cannot contain cycles, since they can only contain
6199 /// other `CachedNode`s that already exist when they are being constructed.
62− #[derive(Debug, Clone)]
63− pub(crate) struct CachedNode<'a> {
100+ #[derive(Debug, Clone, Copy)]
101+ pub(crate) struct CachedNode {
64102 pub id: CacheId,
65− pub _phantom: PhantomData<&'a Node<'a>>,
103+ // Always a copy of the inner cached node's flags when the inner node
104+ // has flags.
105+ pub flags: Option<ElementNodeFlags>,
66106 }
67107}
68108
109+/// The key for keyed children.
110+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
111+pub(crate) struct NodeKey(u32);
112+
69113/// An event listener callback function.
70114///
71115/// It takes three parameters:
@@ -92,6 +136,15 @@ pub struct Attribute<'a> {
92136 pub(crate) value: &'a str,
93137}
94138
139+impl<'a> From<CachedNode> for Node<'a> {
140+ #[inline]
141+ fn from(c: CachedNode) -> Self {
142+ Node {
143+ kind: NodeKind::Cached(c),
144+ }
145+ }
146+}
147+
95148impl fmt::Debug for Listener<'_> {
96149 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
97150 let (a, b) = self.get_callback_parts();
@@ -135,6 +188,7 @@ impl<'a> Node<'a> {
135188 #[inline]
136189 pub(crate) fn element<Listeners, Attributes, Children>(
137190 bump: &'a Bump,
191+ flags: ElementNodeFlags,
138192 tag_name: &'a str,
139193 listeners: Listeners,
140194 attributes: Attributes,
@@ -157,6 +211,7 @@ impl<'a> Node<'a> {
157211
158212 Node {
159213 kind: NodeKind::Element(ElementNode {
214+ flags,
160215 tag_name,
161216 listeners,
162217 attributes,
@@ -174,14 +229,21 @@ impl<'a> Node<'a> {
174229 }
175230 }
176231
177− /// Construct a new cached node with the given id.
178232 #[inline]
179− pub(crate) fn cached(id: CacheId) -> Node<'a> {
180− Node {
181− kind: NodeKind::Cached(CachedNode {
182− id,
183− _phantom: PhantomData,
184− }),
233+ pub(crate) fn flags(&self) -> Option<ElementNodeFlags> {
234+ match &self.kind {
235+ NodeKind::Text(_) => None,
236+ NodeKind::Element(e) => Some(e.flags),
237+ NodeKind::Cached(c) => c.flags,
238+ }
239+ }
240+
241+ #[inline]
242+ pub(crate) fn key(&self) -> NodeKey {
243+ match &self.kind {
244+ NodeKind::Text(_) => NodeKey(u32::MAX),
245+ NodeKind::Element(e) => NodeKey(e.flags.key()),
246+ NodeKind::Cached(c) => NodeKey(c.flags.map_or(u32::MAX, |f| f.key())),
185247 }
186248 }
187249}
src/vdom.rs+2−3
@@ -166,7 +166,8 @@ impl Vdom {
166166
167167 // Create a dummy `<div/>` in our container.
168168 initialize_container(container);
169− let current_root = Node::element(&dom_buffers[0], "div", [], [], [], None);
169+ let current_root =
170+ Node::element(&dom_buffers[0], Default::default(), "div", [], [], [], None);
170171 let current_root = Some(unsafe { extend_node_lifetime(current_root) });
171172
172173 let container = container.clone();
@@ -424,7 +425,6 @@ impl VdomWeak {
424425 /// If you don't want to do more things after the render completes, then use
425426 /// `schedule_render` instead of `render`.
426427 pub fn render(&self) -> impl Future<Item = (), Error = VdomDroppedError> {
427− debug!("VdomWeak::render: initiating render in new animation frame");
428428 futures::future::ok(self.inner.upgrade())
429429 .and_then(|inner| inner.ok_or(()))
430430 .map_err(|_| VdomDroppedError {})
@@ -449,7 +449,6 @@ impl VdomWeak {
449449 // animation frames.
450450 let _ = inner.shared.render_scheduled.take();
451451
452− debug!("VdomWeak::render: finished rendering");
453452 let r = resolve.call0(&JsValue::null());
454453 debug_assert!(r.is_ok());
455454 }
tests/web/keyed.jsnew file+25
@@ -0,0 +1,25 @@
1+export function saveKeyedElements(container) {
2+ const saved = new Map;
3+ const children = container.querySelectorAll(".keyed");
4+ for (const child of children) {
5+ saved.set(child.id, child);
6+ }
7+ return saved;
8+}
9+
10+function assertEq(a, b, msg) {
11+ if (a !== b) {
12+ throw new Error(`assertEq failed: ${a} !== ${b}; ${msg}`);
13+ }
14+}
15+
16+export function checkKeyedElements(container, saved) {
17+ const children = container.querySelectorAll(".keyed");
18+ for (const child of children) {
19+ console.log(`checking child=${child.outerHTML}`);
20+ const original = saved.get(child.id);
21+ if (original) {
22+ assertEq(original, child, `did not preserve child with key=${child.id}`);
23+ }
24+ }
25+}
tests/web/keyed.rsnew file+272
@@ -0,0 +1,272 @@
1+use crate::{assert_rendered, create_element};
2+use dodrio::{builder::*, bumpalo, Node, Render, RenderContext, Vdom};
3+use futures::Future;
4+use log::*;
5+use std::rc::Rc;
6+use wasm_bindgen::prelude::*;
7+use wasm_bindgen_test::*;
8+
9+struct Keyed(u16);
10+
11+impl Render for Keyed {
12+ fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
13+ let key = bumpalo::format!(in cx.bump, "{}", self.0).into_bump_str();
14+ div(&cx)
15+ .attr("class", "keyed")
16+ .attr("id", key)
17+ .key(self.0 as u32)
18+ .finish()
19+ }
20+}
21+
22+fn keyed<'a, Keys>(cx: &mut RenderContext<'a>, keys: Keys) -> Node<'a>
23+where
24+ Keys: AsRef<[u16]>,
25+{
26+ let mut parent = div(&cx).attr("class", "parent").has_keyed_children(true);
27+
28+ for &k in keys.as_ref() {
29+ parent = parent.child(Keyed(k).render(cx));
30+ }
31+
32+ parent.finish()
33+}
34+
35+fn assert_keyed<Before, After>(
36+ before: Before,
37+ after: After,
38+) -> impl Future<Item = (), Error = JsValue>
39+where
40+ Before: 'static + Render,
41+ After: 'static + Render,
42+{
43+ #[wasm_bindgen(module = "/tests/web/keyed.js")]
44+ extern "C" {
45+ #[wasm_bindgen(js_name = saveKeyedElements)]
46+ fn save_keyed_elements(container: &web_sys::Element) -> JsValue;
47+
48+ #[wasm_bindgen(js_name = checkKeyedElements)]
49+ fn check_keyed_elements(container: &web_sys::Element, saved: JsValue);
50+ }
51+
52+ let container = create_element("div");
53+
54+ let before = Rc::new(before);
55+ let after = Rc::new(after);
56+
57+ debug!("====== Rendering the *before* DOM into the physical DOM ======");
58+ let vdom1 = Rc::new(Vdom::new(&container, before.clone()));
59+ let vdom2 = vdom1.clone();
60+ let saved = save_keyed_elements(&container);
61+
62+ debug!("====== Checking the *before* DOM against the physical DOM ======");
63+ assert_rendered(&container, &before);
64+
65+ debug!("====== Rendering the *after* DOM into the physical DOM ======");
66+ let weak = vdom1.weak();
67+ weak.set_component(Box::new(after.clone()))
68+ .map(move |_| {
69+ debug!("====== Checking the *after* DOM against the physical DOM ======");
70+ assert_rendered(&container, &after);
71+ check_keyed_elements(&container, saved);
72+ drop(vdom2);
73+ })
74+ .map_err(|e| JsValue::from(e.to_string()))
75+}
76+
77+macro_rules! keyed_tests {
78+ ( $(
79+ $name:ident {
80+ before($before_cx:ident) {
81+ $( $before:tt )*
82+ }
83+ after($after_cx:ident) {
84+ $( $after:tt )*
85+ }
86+ }
87+ )* ) => {
88+ $(
89+ #[wasm_bindgen_test(async)]
90+ fn $name() -> impl Future<Item = (), Error = wasm_bindgen::JsValue> {
91+ use crate::RenderFn;
92+ log::debug!("############### {} ###############", stringify!($name));
93+ assert_keyed(
94+ RenderFn(|$before_cx| { $( $before )* }),
95+ RenderFn(|$after_cx| { $( $after )* }),
96+ )
97+ }
98+ )*
99+ }
100+}
101+
102+keyed_tests! {
103+ same_order {
104+ before(cx) {
105+ keyed(cx, [1, 2, 3])
106+ }
107+ after(cx) {
108+ keyed(cx, [1, 2, 3])
109+ }
110+ }
111+
112+ same_order_append {
113+ before(cx) {
114+ keyed(cx, [1])
115+ }
116+ after(cx) {
117+ keyed(cx, [1, 2])
118+ }
119+ }
120+
121+ same_order_delete {
122+ before(cx) {
123+ keyed(cx, [1, 2, 3])
124+ }
125+ after(cx) {
126+ keyed(cx, [1, 2])
127+ }
128+ }
129+
130+ same_suffix {
131+ before(cx) {
132+ keyed(cx, [1, 2, 3])
133+ }
134+ after(cx) {
135+ keyed(cx, [4, 2, 3])
136+ }
137+ }
138+
139+ same_prefix_and_suffix_reorder_middle {
140+ before(cx) {
141+ keyed(cx, [1, 2, 3, 4])
142+ }
143+ after(cx) {
144+ keyed(cx, [1, 3, 2, 4])
145+ }
146+ }
147+
148+ same_prefix_and_suffix_new_middle {
149+ before(cx) {
150+ keyed(cx, [2, 3, 4, 5])
151+ }
152+ after(cx) {
153+ keyed(cx, [2, 7, 8, 5])
154+ }
155+ }
156+
157+ reverse_order {
158+ before(cx) {
159+ keyed(cx, [1, 2, 3])
160+ }
161+ after(cx) {
162+ keyed(cx, [3, 2, 1])
163+ }
164+ }
165+
166+ no_shared_keys {
167+ before(cx) {
168+ keyed(cx, [1, 2])
169+ }
170+ after(cx) {
171+ keyed(cx, [3, 4])
172+ }
173+ }
174+
175+ new_keys_in_middle {
176+ before(cx) {
177+ keyed(cx, [1, 2])
178+ }
179+ after(cx) {
180+ keyed(cx, [1, 3, 4, 2])
181+ }
182+ }
183+
184+ new_keys_at_start_and_end {
185+ before(cx) {
186+ keyed(cx, [1, 2, 3])
187+ }
188+ after(cx) {
189+ keyed(cx, [4, 1, 2, 3, 5])
190+ }
191+ }
192+
193+ delete_prefix {
194+ before(cx) {
195+ keyed(cx, [1, 2, 3])
196+ }
197+ after(cx) {
198+ keyed(cx, [2, 3])
199+ }
200+ }
201+
202+ delete_suffix {
203+ before(cx) {
204+ keyed(cx, [1, 2, 3])
205+ }
206+ after(cx) {
207+ keyed(cx, [1, 2])
208+ }
209+ }
210+
211+ delete_middle {
212+ before(cx) {
213+ keyed(cx, [1, 2, 3])
214+ }
215+ after(cx) {
216+ keyed(cx, [1, 3])
217+ }
218+ }
219+
220+ nested_keyed_children {
221+ before(cx) {
222+ ul(&cx)
223+ .has_keyed_children(true)
224+ .children([
225+ li(&cx)
226+ .key(1)
227+ .children([
228+ keyed(cx, [2, 3, 4])
229+ ])
230+ .finish(),
231+ li(&cx)
232+ .key(5)
233+ .children([
234+ keyed(cx, [6, 7, 8])
235+ ])
236+ .finish(),
237+ li(&cx)
238+ .key(9)
239+ .children([
240+ keyed(cx, [10, 11, 12])
241+ ])
242+ .finish(),
243+ ])
244+ .finish()
245+ }
246+ after(cx) {
247+ ul(&cx)
248+ .has_keyed_children(true)
249+ .children([
250+ li(&cx)
251+ .key(9)
252+ .children([
253+ keyed(cx, [12, 11, 10])
254+ ])
255+ .finish(),
256+ li(&cx)
257+ .key(5)
258+ .children([
259+ keyed(cx, [8, 7, 6])
260+ ])
261+ .finish(),
262+ li(&cx)
263+ .key(1)
264+ .children([
265+ keyed(cx, [4, 3, 2])
266+ ])
267+ .finish(),
268+ ])
269+ .finish()
270+ }
271+ }
272+}
tests/web/main.rs+1
@@ -19,6 +19,7 @@ wasm_bindgen_test_configure!(run_in_browser);
1919pub mod cached;
2020pub mod events;
2121pub mod js_api;
22+pub mod keyed;
2223pub mod render;
2324
2425pub fn window() -> web_sys::Window {