on master help sign in
Support keyed children
ad5f96eNick Fitzgerald
Cargo.lock+8
@@ -205,6 +205,7 @@ dependencies = [
205name = "dodrio"
205name = "dodrio"
206version = "0.1.0"
206version = "0.1.0"
207dependencies = [
207dependencies = [
208+ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
208 "bumpalo 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
209 "bumpalo 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
209 "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
210 "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
210 "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
211 "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -214,6 +215,7 @@ dependencies = [
214 "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
215 "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
215 "js-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
216 "js-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
216 "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
217 "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)",
217 "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
219 "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
218 "wasm-bindgen-futures 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
220 "wasm-bindgen-futures 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
219 "wasm-bindgen-test 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
221 "wasm-bindgen-test 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -453,6 +455,11 @@ dependencies = [
453 "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
455 "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
454]
456]
455
457
458+[[package]]
459+name = "longest-increasing-subsequence"
460+version = "0.1.0"
461+source = "registry+https://github.com/rust-lang/crates.io-index"
462+
456[[package]]
463[[package]]
457name = "memchr"
464name = "memchr"
458version = "2.2.0"
465version = "2.2.0"
@@ -968,6 +975,7 @@ dependencies = [
968"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
975"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
969"checksum libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "bedcc7a809076656486ffe045abeeac163da1b558e963a31e29fbfbeba916917"
976"checksum libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "bedcc7a809076656486ffe045abeeac163da1b558e963a31e29fbfbeba916917"
970"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
977"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"
971"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
979"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
972"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3"
980"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3"
973"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
981"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"
42js-sys = "0.3.17"
42js-sys = "0.3.17"
43fxhash = "0.2.1"
43fxhash = "0.2.1"
44log = { version = "0.4.6", optional = true }
44log = { version = "0.4.6", optional = true }
45+bitflags = "1.0.4"
46+longest-increasing-subsequence = "0.1.0"
45
47
46[dependencies.web-sys]
48[dependencies.web-sys]
47version = "0.3.17"
49version = "0.3.17"
js/change-list-interpreter.js+53−7
@@ -188,13 +188,57 @@ const OP_TABLE = [
188 },
188 },
189
189
190 // 17
190 // 17
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
191− function setAttributeNS(interpreter, mem8, mem32, i) {
204+ function pushChild(interpreter, mem8, mem32, i) {
205+ const parent = top(interpreter.stack);
192− const nameId = mem32[i++];
206+ const n = mem32[i++];
193− const valueId = 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) {
194− const name = interpreter.getCachedString(nameId);
221+ const before = interpreter.stack.pop();
195− const value = interpreter.getCachedString(valueId);
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) {
196− const node = top(interpreter.stack);
230+ const parent = top(interpreter.stack);
197− node.setAttributeNS(null, name, value);
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();
198 return i;
242 return i;
199 }
243 }
200];
244];
@@ -206,6 +250,7 @@ export class ChangeListInterpreter {
206 this.ranges = [];
250 this.ranges = [];
207 this.stack = [];
251 this.stack = [];
208 this.strings = new Map();
252 this.strings = new Map();
253+ this.temporaries = [];
209 }
254 }
210
255
211 unmount() {
256 unmount() {
@@ -218,6 +263,7 @@ export class ChangeListInterpreter {
218 this.ranges = null;
263 this.ranges = null;
219 this.stack = null;
264 this.stack = null;
220 this.strings = null;
265 this.strings = null;
266+ this.temporaries = null;
221 }
267 }
222
268
223 addChangeListRange(start, len) {
269 addChangeListRange(start, len) {
src/builder.rs+59−2
@@ -1,6 +1,6 @@
1//! Helpers for building virtual DOM nodes.
1//! Helpers for building virtual DOM nodes.
2
2
3−use crate::{Attribute, Listener, Node, RootRender, VdomWeak};
3+use crate::{node::ElementNodeFlags, Attribute, Listener, Node, RootRender, VdomWeak};
4use bumpalo::Bump;
4use bumpalo::Bump;
5
5
6/// A virtual DOM element builder.
6/// A virtual DOM element builder.
@@ -16,6 +16,7 @@ where
16 Children: 'a + AsRef<[Node<'a>]>,
16 Children: 'a + AsRef<[Node<'a>]>,
17{
17{
18 bump: &'a Bump,
18 bump: &'a Bump,
19+ flags: ElementNodeFlags,
19 tag_name: &'a str,
20 tag_name: &'a str,
20 listeners: Listeners,
21 listeners: Listeners,
21 attributes: Attributes,
22 attributes: Attributes,
@@ -61,6 +62,7 @@ impl<'a>
61 let bump = bump.into();
62 let bump = bump.into();
62 ElementBuilder {
63 ElementBuilder {
63 bump,
64 bump,
65+ flags: Default::default(),
64 tag_name,
66 tag_name,
65 listeners: bumpalo::collections::Vec::new_in(bump),
67 listeners: bumpalo::collections::Vec::new_in(bump),
66 attributes: bumpalo::collections::Vec::new_in(bump),
68 attributes: bumpalo::collections::Vec::new_in(bump),
@@ -110,6 +112,7 @@ where
110 {
112 {
111 ElementBuilder {
113 ElementBuilder {
112 bump: self.bump,
114 bump: self.bump,
115+ flags: self.flags,
113 tag_name: self.tag_name,
116 tag_name: self.tag_name,
114 listeners,
117 listeners,
115 attributes: self.attributes,
118 attributes: self.attributes,
@@ -148,6 +151,7 @@ where
148 {
151 {
149 ElementBuilder {
152 ElementBuilder {
150 bump: self.bump,
153 bump: self.bump,
154+ flags: self.flags,
151 tag_name: self.tag_name,
155 tag_name: self.tag_name,
152 listeners: self.listeners,
156 listeners: self.listeners,
153 attributes,
157 attributes,
@@ -186,6 +190,7 @@ where
186 {
190 {
187 ElementBuilder {
191 ElementBuilder {
188 bump: self.bump,
192 bump: self.bump,
193+ flags: self.flags,
189 tag_name: self.tag_name,
194 tag_name: self.tag_name,
190 listeners: self.listeners,
195 listeners: self.listeners,
191 attributes: self.attributes,
196 attributes: self.attributes,
@@ -204,7 +209,7 @@ where
204 /// let b = Bump::new();
209 /// let b = Bump::new();
205 ///
210 ///
206 /// // Create a `<td>` tag with an xhtml namespace
211 /// // Create a `<td>` tag with an xhtml namespace
207− /// let my_div = td(&b)
212+ /// let my_td = td(&b)
208 /// .namespace(Some("http://www.w3.org/1999/xhtml"))
213 /// .namespace(Some("http://www.w3.org/1999/xhtml"))
209 /// .finish();
214 /// .finish();
210 /// ```
215 /// ```
@@ -212,6 +217,7 @@ where
212 pub fn namespace(self, namespace: Option<&'a str>) -> Self {
217 pub fn namespace(self, namespace: Option<&'a str>) -> Self {
213 ElementBuilder {
218 ElementBuilder {
214 bump: self.bump,
219 bump: self.bump,
220+ flags: self.flags,
215 tag_name: self.tag_name,
221 tag_name: self.tag_name,
216 listeners: self.listeners,
222 listeners: self.listeners,
217 attributes: self.attributes,
223 attributes: self.attributes,
@@ -220,6 +226,56 @@ where
220 }
226 }
221 }
227 }
222
228
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+
223 /// Create the virtual DOM node described by this builder.
279 /// Create the virtual DOM node described by this builder.
224 ///
280 ///
225 /// # Example
281 /// # Example
@@ -239,6 +295,7 @@ where
239 pub fn finish(self) -> Node<'a> {
295 pub fn finish(self) -> Node<'a> {
240 Node::element(
296 Node::element(
241 self.bump,
297 self.bump,
298+ self.flags,
242 self.tag_name,
299 self.tag_name,
243 self.listeners,
300 self.listeners,
244 self.attributes,
301 self.attributes,
src/cached.rs+16−13
@@ -1,7 +1,4 @@
1−use crate::{
2− cached_set::{CacheId, CachedSet},
3− Node, Render, RenderContext,
1+use crate::{cached_set::CachedSet, node::CachedNode, Node, Render, RenderContext};
4−};
5use std::cell::Cell;
2use std::cell::Cell;
6use std::ops::{Deref, DerefMut};
3use std::ops::{Deref, DerefMut};
7
4
@@ -10,7 +7,7 @@ use std::ops::{Deref, DerefMut};
10#[derive(Clone, Debug)]
7#[derive(Clone, Debug)]
11pub struct Cached<R> {
8pub struct Cached<R> {
12 inner: R,
9 inner: R,
13− cached: Cell<Option<CacheId>>,
10+ cached: Cell<Option<CachedNode>>,
14}
11}
15
12
16impl<R> Cached<R> {
13impl<R> Cached<R> {
@@ -112,7 +109,7 @@ where
112 R: Render,
109 R: Render,
113{
110{
114 fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
111 fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
115− let id = match self.cached.get() {
112+ let cached = match self.cached.get() {
116 // This does-the-cache-contain-this-id check is necessary because
113 // This does-the-cache-contain-this-id check is necessary because
117 // the same `Cached<R>` instance can be rendered into vdom A, which
114 // the same `Cached<R>` instance can be rendered into vdom A, which
118 // will save the results into A's cached set and yield id X. Then,
115 // will save the results into A's cached set and yield id X. Then,
@@ -140,21 +137,27 @@ where
140 //
137 //
141 // This is all do-able but is a bit more book keeping than we really
138 // This is all do-able but is a bit more book keeping than we really
142 // want to do unless it is well motivated.
139 // want to do unless it is well motivated.
143− Some(id)
140+ Some(cached)
144 if {
141 if {
145 let cached_set = cx.cached_set.borrow();
142 let cached_set = cx.cached_set.borrow();
146− cached_set.contains(id)
143+ cached_set.contains(cached.id)
147 } =>
144 } =>
148 {
145 {
149− id
146+ cached
150 }
147 }
151 _ => {
148 _ => {
149+ let mut flags = Default::default();
152− let id = CachedSet::insert(cx, |nested_cx| self.inner.render(nested_cx));
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 };
153− self.cached.set(Some(id));
156+ self.cached.set(Some(cached));
154− id
157+ cached
155 }
158 }
156 };
159 };
157
160
158− Node::cached(id)
161+ cached.into()
159 }
162 }
160}
163}
src/cached_set.rs−3
@@ -64,7 +64,6 @@ impl CachedSet {
64 self.items.retain(|id, entry| {
64 self.items.retain(|id, entry| {
65 let keep = marked.contains(id);
65 let keep = marked.contains(id);
66 if !keep {
66 if !keep {
67− debug!("CachedSet::gc: removing {:?}", id);
68 let node: &Node = unsafe { &*entry.node };
67 let node: &Node = unsafe { &*entry.node };
69 registry.remove_subtree(node);
68 registry.remove_subtree(node);
70 }
69 }
@@ -129,7 +128,6 @@ impl CachedSet {
129
128
130 let mut set = set.borrow_mut();
129 let mut set = set.borrow_mut();
131 let id = set.next_id();
130 let id = set.next_id();
132− debug!("CachedSet::insert: id = {:?}; entry = {:?}", id, entry);
133 set.items.insert(id, entry);
131 set.items.insert(id, entry);
134 id
132 id
135 }
133 }
@@ -141,7 +139,6 @@ impl CachedSet {
141
139
142 /// Get the node for the given cache id.
140 /// Get the node for the given cache id.
143 pub fn get(&self, mut id: CacheId) -> &Node {
141 pub fn get(&self, mut id: CacheId) -> &Node {
144− debug!("CachedSet::get: id = {:?}", id);
145 loop {
142 loop {
146 let entry = self
143 let entry = self
147 .items
144 .items
src/change_list/emitter.rs+56
@@ -201,4 +201,60 @@ define_change_list_instructions! {
201 /// stack.push(document.createElementNS(tag_name, namespace))
201 /// stack.push(document.createElementNS(tag_name, namespace))
202 /// ```
202 /// ```
203 create_element_ns(tag_name_key, namespace_key) = 16,
203 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,
204}
260}
src/change_list/mod.rs+52−2
@@ -17,11 +17,11 @@ pub(crate) struct ChangeListPersistentState {
17
17
18pub(crate) struct ChangeListBuilder<'a> {
18pub(crate) struct ChangeListBuilder<'a> {
19 state: &'a mut ChangeListPersistentState,
19 state: &'a mut ChangeListPersistentState,
20+ next_temporary: u32,
20}
21}
21
22
22impl Drop for ChangeListPersistentState {
23impl Drop for ChangeListPersistentState {
23 fn drop(&mut self) {
24 fn drop(&mut self) {
24− debug!("Dropping ChangeListPersistentState");
25 self.interpreter.unmount();
25 self.interpreter.unmount();
26 }
26 }
27}
27}
@@ -43,7 +43,10 @@ impl ChangeListPersistentState {
43 }
43 }
44
44
45 pub(crate) fn builder<'a>(&'a mut self) -> ChangeListBuilder<'a> {
45 pub(crate) fn builder<'a>(&'a mut self) -> ChangeListBuilder<'a> {
46− ChangeListBuilder { state: self }
46+ ChangeListBuilder {
47+ state: self,
48+ next_temporary: 0,
49+ }
47 }
50 }
48}
51}
49
52
@@ -77,6 +80,53 @@ cfg_if::cfg_if! {
77}
80}
78
81
79impl ChangeListBuilder<'_> {
82impl 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+
80 pub fn ensure_string(&mut self, string: &str) -> StringKey {
130 pub fn ensure_string(&mut self, string: &str) -> StringKey {
81 self.state
131 self.state
82 .strings
132 .strings
src/change_list/strings.rs+4−3
@@ -45,8 +45,7 @@ impl StringsCache {
45 }
45 }
46
46
47 pub fn drop_unused_strings(&mut self, emitter: &InstructionEmitter) {
47 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| {
50 if entry.used {
49 if entry.used {
51 // Since this entry was used during while rendering this frame,
50 // Since this entry was used during while rendering this frame,
52 // it is likely going to be used for the next frame as well. So
51 // it is likely going to be used for the next frame as well. So
@@ -57,7 +56,9 @@ impl StringsCache {
57 entry.used = false;
56 entry.used = false;
58 true
57 true
59 } else {
58 } 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);
61 false
62 false
62 }
63 }
63 });
64 });
src/diff.rs+625−51
@@ -2,11 +2,22 @@ use crate::{
2 cached_set::{CacheId, CachedSet},
2 cached_set::{CacheId, CachedSet},
3 change_list::ChangeListBuilder,
3 change_list::ChangeListBuilder,
4 events::EventsRegistry,
4 events::EventsRegistry,
5− node::{Attribute, ElementNode, Listener, Node, NodeKind, TextNode},
5+ node::{Attribute, ElementNode, ElementNodeFlags, Listener, Node, NodeKind, TextNode},
6};
6};
7−use fxhash::FxHashSet;
7+use fxhash::{FxHashMap, FxHashSet};
8−use std::cmp;
8+use std::cmp::Ordering;
9+use std::ops::Range;
10+use std::u32;
9
11
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.
10pub(crate) fn diff(
21pub(crate) fn diff(
11 cached_set: &CachedSet,
22 cached_set: &CachedSet,
12 change_list: &mut ChangeListBuilder,
23 change_list: &mut ChangeListBuilder,
@@ -20,22 +31,18 @@ pub(crate) fn diff(
20 &NodeKind::Text(TextNode { text: new_text }),
31 &NodeKind::Text(TextNode { text: new_text }),
21 &NodeKind::Text(TextNode { text: old_text }),
32 &NodeKind::Text(TextNode { text: old_text }),
22 ) => {
33 ) => {
23− debug!(" both are text nodes");
24 if new_text != old_text {
34 if new_text != old_text {
25− debug!(" text needs updating");
26 change_list.set_text(new_text);
35 change_list.set_text(new_text);
27 }
36 }
28 }
37 }
29
38
30 (&NodeKind::Text(_), &NodeKind::Element(_)) => {
39 (&NodeKind::Text(_), &NodeKind::Element(_)) => {
31− debug!(" replacing a text node with an element");
32 create(cached_set, change_list, registry, new, cached_roots);
40 create(cached_set, change_list, registry, new, cached_roots);
33 registry.remove_subtree(&old);
41 registry.remove_subtree(&old);
34 change_list.replace_with();
42 change_list.replace_with();
35 }
43 }
36
44
37 (&NodeKind::Element(_), &NodeKind::Text(_)) => {
45 (&NodeKind::Element(_), &NodeKind::Text(_)) => {
38− debug!(" replacing an element with a text node");
39 create(cached_set, change_list, registry, new, cached_roots);
46 create(cached_set, change_list, registry, new, cached_roots);
40 // Note: text nodes cannot have event listeners, so we don't need to
47 // Note: text nodes cannot have event listeners, so we don't need to
41 // remove the old node's listeners from our registry her.
48 // remove the old node's listeners from our registry her.
@@ -44,6 +51,7 @@ pub(crate) fn diff(
44
51
45 (
52 (
46 &NodeKind::Element(ElementNode {
53 &NodeKind::Element(ElementNode {
54+ flags: new_flags,
47 tag_name: new_tag_name,
55 tag_name: new_tag_name,
48 listeners: new_listeners,
56 listeners: new_listeners,
49 attributes: new_attributes,
57 attributes: new_attributes,
@@ -51,6 +59,7 @@ pub(crate) fn diff(
51 namespace: new_namespace,
59 namespace: new_namespace,
52 }),
60 }),
53 &NodeKind::Element(ElementNode {
61 &NodeKind::Element(ElementNode {
62+ flags: old_flags,
54 tag_name: old_tag_name,
63 tag_name: old_tag_name,
55 listeners: old_listeners,
64 listeners: old_listeners,
56 attributes: old_attributes,
65 attributes: old_attributes,
@@ -58,9 +67,7 @@ pub(crate) fn diff(
58 namespace: old_namespace,
67 namespace: old_namespace,
59 }),
68 }),
60 ) => {
69 ) => {
61− debug!(" updating an element");
62 if new_tag_name != old_tag_name || new_namespace != old_namespace {
70 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");
64 create(cached_set, change_list, registry, new, cached_roots);
71 create(cached_set, change_list, registry, new, cached_roots);
65 registry.remove_subtree(&old);
72 registry.remove_subtree(&old);
66 change_list.replace_with();
73 change_list.replace_with();
@@ -72,6 +79,8 @@ pub(crate) fn diff(
72 cached_set,
79 cached_set,
73 change_list,
80 change_list,
74 registry,
81 registry,
82+ new_flags,
83+ old_flags,
75 old_children,
84 old_children,
76 new_children,
85 new_children,
77 cached_roots,
86 cached_roots,
@@ -113,14 +122,19 @@ pub(crate) fn diff(
113 }
122 }
114}
123}
115
124
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.
116fn diff_listeners(
132fn diff_listeners(
117 change_list: &mut ChangeListBuilder,
133 change_list: &mut ChangeListBuilder,
118 registry: &mut EventsRegistry,
134 registry: &mut EventsRegistry,
119 old: &[Listener],
135 old: &[Listener],
120 new: &[Listener],
136 new: &[Listener],
121) {
137) {
122− debug!(" updating event listeners");
123−
124 'outer1: for new_l in new {
138 'outer1: for new_l in new {
125 unsafe {
139 unsafe {
126 // Safety relies on removing `new_l` from the registry manually at
140 // Safety relies on removing `new_l` from the registry manually at
@@ -151,9 +165,14 @@ fn diff_listeners(
151 }
165 }
152}
166}
153
167
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.
154fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new: &[Attribute]) {
175fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new: &[Attribute]) {
155− debug!(" updating attributes");
156−
157 // Do O(n^2) passes to add/update and remove attributes, since
176 // Do O(n^2) passes to add/update and remove attributes, since
158 // there are almost always very few attributes.
177 // there are almost always very few attributes.
159 'outer: for new_attr in new {
178 'outer: for new_attr in new {
@@ -182,104 +201,629 @@ fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new:
182 }
201 }
183}
202}
184
203
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.
185fn diff_children(
211fn diff_children(
186 cached_set: &CachedSet,
212 cached_set: &CachedSet,
187 change_list: &mut ChangeListBuilder,
213 change_list: &mut ChangeListBuilder,
188 registry: &mut EventsRegistry,
214 registry: &mut EventsRegistry,
215+ new_flags: ElementNodeFlags,
216+ old_flags: ElementNodeFlags,
189 old: &[Node],
217 old: &[Node],
190 new: &[Node],
218 new: &[Node],
191 cached_roots: &mut FxHashSet<CacheId>,
219 cached_roots: &mut FxHashSet<CacheId>,
192) {
220) {
193 if new.is_empty() {
221 if new.is_empty() {
194 if !old.is_empty() {
222 if !old.is_empty() {
195− remove_children(change_list, registry, old);
223+ remove_all_children(change_list, registry, old);
196 }
224 }
197 return;
225 return;
198 }
226 }
199
227
200 if old.is_empty() {
228 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);
202 return;
230 return;
203 }
231 }
204
232
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+}
206
338
207− let num_children_to_diff = cmp::min(new.len(), old.len());
339+// Diff the prefix of children in `new` and `old` that share the same keys in
208− let mut new_children = new.iter();
340+// the same order.
209− let mut old_children = old.iter();
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 {
210 let mut pushed = false;
355 let mut pushed = false;
356+ let mut shared_prefix_count = 0;
211
357
212− for (i, (new_child, old_child)) in new_children
358+ for (old, new) in old.iter().zip(new.iter()) {
213− .by_ref()
359+ if old.key() != new.key() {
214− .zip(old_children.by_ref())
360+ break;
215− .take(num_children_to_diff)
361+ }
216− .enumerate()
362+
217− {
363+ if pushed {
218− if i == 0 {
364+ debug_assert!(shared_prefix_count > 0);
365+ change_list.pop_push_next_sibling();
366+ } else {
367+ debug_assert_eq!(shared_prefix_count, 0);
219 change_list.push_first_child();
368 change_list.push_first_child();
220 pushed = true;
369 pushed = true;
221− } else {
222− debug_assert!(pushed);
223− change_list.pop_push_next_sibling();
224 }
370 }
225
371
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(
227 cached_set,
392 cached_set,
228 change_list,
393 change_list,
229 registry,
394 registry,
230− old_child,
395+ &new[shared_prefix_count..],
231− new_child,
232 cached_roots,
396 cached_roots,
233 );
397 );
398+ return KeyedPrefixResult::Finished;
234 }
399 }
235
400
236− if old_children.next().is_some() {
401+ // And if that was all of the new children, then remove all of the remaining
237− debug!(" removing extra old children");
402+ // old children and we're finished.
238− debug_assert!(new_children.next().is_none());
403+ if shared_prefix_count == new.len() {
239− if pushed {
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 {
240 change_list.pop_push_next_sibling();
480 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;
241 } else {
529 } else {
242− change_list.push_first_child();
530+ start = end + 1;
243 }
531 }
244− change_list.remove_self_and_next_siblings();
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.
245− pushed = false;
545+ let mut pushed = false;
246− } else {
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
247− debug!(" creating new children");
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);
248 if pushed {
592 if pushed {
249 change_list.pop();
593 change_list.pop();
250− pushed = false;
251 }
594 }
252− create_children(
595+ change_list.push_temporary(temp);
596+ pushed = true;
597+ diff(
253 cached_set,
598 cached_set,
254 change_list,
599 change_list,
255 registry,
600 registry,
256− new_children,
601+ &old[old_index],
602+ new_child,
257 cached_roots,
603 cached_roots,
258 );
604 );
605+
606+ segment_end = lis_index;
259 }
607 }
260
608
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+
262 if pushed {
624 if pushed {
263 change_list.pop();
625 change_list.pop();
264 }
626 }
265}
627}
266
628
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(
268 cached_set: &CachedSet,
807 cached_set: &CachedSet,
269 change_list: &mut ChangeListBuilder,
808 change_list: &mut ChangeListBuilder,
270 registry: &mut EventsRegistry,
809 registry: &mut EventsRegistry,
271− new: I,
810+ new: &[Node],
272 cached_roots: &mut FxHashSet<CacheId>,
811 cached_roots: &mut FxHashSet<CacheId>,
273−) where
274− I: IntoIterator<Item = &'a Node<'a>>,
275−{
812+) {
276 for child in new {
813 for child in new {
277 create(cached_set, change_list, registry, child, cached_roots);
814 create(cached_set, change_list, registry, child, cached_roots);
278 change_list.append_child();
815 change_list.append_child();
279 }
816 }
280}
817}
281
818
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(
283 change_list: &mut ChangeListBuilder,
827 change_list: &mut ChangeListBuilder,
284 registry: &mut EventsRegistry,
828 registry: &mut EventsRegistry,
285 old: &[Node],
829 old: &[Node],
@@ -292,6 +836,35 @@ fn remove_children(
292 change_list.set_text("");
836 change_list.set_text("");
293}
837}
294
838
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]
295fn create(
868fn create(
296 cached_set: &CachedSet,
869 cached_set: &CachedSet,
297 change_list: &mut ChangeListBuilder,
870 change_list: &mut ChangeListBuilder,
@@ -304,6 +877,7 @@ fn create(
304 change_list.create_text_node(text);
877 change_list.create_text_node(text);
305 }
878 }
306 NodeKind::Element(ElementNode {
879 NodeKind::Element(ElementNode {
880+ flags: _,
307 tag_name,
881 tag_name,
308 listeners,
882 listeners,
309 attributes,
883 attributes,
src/events.rs−4
@@ -64,7 +64,6 @@ cfg_if::cfg_if! {
64
64
65 let weak_registry = Rc::downgrade(&registry);
65 let weak_registry = Rc::downgrade(&registry);
66 let closure = Closure::wrap(Box::new(move |event, a, b| {
66 let closure = Closure::wrap(Box::new(move |event, a, b| {
67− debug!("Events trampoline invoked with (0x{:x}, 0x{:x})", a, b);
68 debug_assert!(a != 0);
67 debug_assert!(a != 0);
69
68
70 // if the VdomInnerExclusive is keeping this closure alive, then the
69 // if the VdomInnerExclusive is keeping this closure alive, then the
@@ -95,7 +94,6 @@ cfg_if::cfg_if! {
95
94
96 pub(crate) fn remove(&mut self, listener: &Listener) {
95 pub(crate) fn remove(&mut self, listener: &Listener) {
97 let id = listener.get_callback_parts();
96 let id = listener.get_callback_parts();
98− debug!("EventsRegistry::remove(0x{:x}, 0x{:x})", id.0, id.1);
99 debug_assert!(id.0 != 0);
97 debug_assert!(id.0 != 0);
100 self.active.remove(&id);
98 self.active.remove(&id);
101 }
99 }
@@ -128,7 +126,6 @@ cfg_if::cfg_if! {
128 /// diffing.
126 /// diffing.
129 pub(crate) unsafe fn add<'a>(&mut self, listener: &'a Listener<'a>) {
127 pub(crate) unsafe fn add<'a>(&mut self, listener: &'a Listener<'a>) {
130 let id = listener.get_callback_parts();
128 let id = listener.get_callback_parts();
131− debug!("EventsRegistry::add(0x{:x}, 0x{:x})", id.0, id.1);
132 debug_assert!(id.0 != 0);
129 debug_assert!(id.0 != 0);
133
130
134 let callback =
131 let callback =
@@ -139,7 +136,6 @@ cfg_if::cfg_if! {
139
136
140 /// Clear all event listeners from the registry.
137 /// Clear all event listeners from the registry.
141 pub(crate) fn clear_active_listeners(&mut self) {
138 pub(crate) fn clear_active_listeners(&mut self) {
142− debug!("EventsRegistry::clear_active_listeners()");
143 self.active.clear();
139 self.active.clear();
144 }
140 }
145 }
141 }
src/node.rs+74−12
@@ -2,8 +2,8 @@ use crate::{cached_set::CacheId, RootRender, VdomWeak};
2use bumpalo::Bump;
2use bumpalo::Bump;
3use std::fmt;
3use std::fmt;
4use std::iter;
4use std::iter;
5−use std::marker::PhantomData;
6use std::mem;
5use std::mem;
6+use std::u32;
7
7
8/// A virtual DOM node.
8/// A virtual DOM node.
9#[derive(Debug, Clone)]
9#[derive(Debug, Clone)]
@@ -28,7 +28,7 @@ pub_unstable_internal! {
28
28
29 /// A node in the vdom's `CachedSet`. This allows us to avoid
29 /// A node in the vdom's `CachedSet`. This allows us to avoid
30 /// re-rendering and re-diffing subtrees.
30 /// re-rendering and re-diffing subtrees.
31− Cached(CachedNode<'a>),
31+ Cached(CachedNode),
32 }
32 }
33}
33}
34
34
@@ -41,11 +41,49 @@ pub_unstable_internal! {
41 }
41 }
42}
42}
43
43
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+
44pub_unstable_internal! {
81pub_unstable_internal! {
45 /// Elements have a tag name, zero or more attributes, and zero or more
82 /// Elements have a tag name, zero or more attributes, and zero or more
46 /// children.
83 /// children.
47 #[derive(Debug, Clone)]
84 #[derive(Debug, Clone)]
48 pub(crate) struct ElementNode<'a> {
85 pub(crate) struct ElementNode<'a> {
86+ pub flags: ElementNodeFlags,
49 pub tag_name: &'a str,
87 pub tag_name: &'a str,
50 pub listeners: &'a [Listener<'a>],
88 pub listeners: &'a [Listener<'a>],
51 pub attributes: &'a [Attribute<'a>],
89 pub attributes: &'a [Attribute<'a>],
@@ -59,13 +97,19 @@ pub_unstable_internal! {
59 /// allows us to avoid both re-rendering a sub-tree and re-diffing
97 /// allows us to avoid both re-rendering a sub-tree and re-diffing
60 /// it. `CachedNode`s cannot contain cycles, since they can only contain
98 /// it. `CachedNode`s cannot contain cycles, since they can only contain
61 /// other `CachedNode`s that already exist when they are being constructed.
99 /// other `CachedNode`s that already exist when they are being constructed.
62− #[derive(Debug, Clone)]
100+ #[derive(Debug, Clone, Copy)]
63− pub(crate) struct CachedNode<'a> {
101+ pub(crate) struct CachedNode {
64 pub id: CacheId,
102 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>,
66 }
106 }
67}
107}
68
108
109+/// The key for keyed children.
110+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
111+pub(crate) struct NodeKey(u32);
112+
69/// An event listener callback function.
113/// An event listener callback function.
70///
114///
71/// It takes three parameters:
115/// It takes three parameters:
@@ -92,6 +136,15 @@ pub struct Attribute<'a> {
92 pub(crate) value: &'a str,
136 pub(crate) value: &'a str,
93}
137}
94
138
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+
95impl fmt::Debug for Listener<'_> {
148impl fmt::Debug for Listener<'_> {
96 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
149 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
97 let (a, b) = self.get_callback_parts();
150 let (a, b) = self.get_callback_parts();
@@ -135,6 +188,7 @@ impl<'a> Node<'a> {
135 #[inline]
188 #[inline]
136 pub(crate) fn element<Listeners, Attributes, Children>(
189 pub(crate) fn element<Listeners, Attributes, Children>(
137 bump: &'a Bump,
190 bump: &'a Bump,
191+ flags: ElementNodeFlags,
138 tag_name: &'a str,
192 tag_name: &'a str,
139 listeners: Listeners,
193 listeners: Listeners,
140 attributes: Attributes,
194 attributes: Attributes,
@@ -157,6 +211,7 @@ impl<'a> Node<'a> {
157
211
158 Node {
212 Node {
159 kind: NodeKind::Element(ElementNode {
213 kind: NodeKind::Element(ElementNode {
214+ flags,
160 tag_name,
215 tag_name,
161 listeners,
216 listeners,
162 attributes,
217 attributes,
@@ -174,14 +229,21 @@ impl<'a> Node<'a> {
174 }
229 }
175 }
230 }
176
231
177− /// Construct a new cached node with the given id.
178 #[inline]
232 #[inline]
179− pub(crate) fn cached(id: CacheId) -> Node<'a> {
233+ pub(crate) fn flags(&self) -> Option<ElementNodeFlags> {
180− Node {
234+ match &self.kind {
181− kind: NodeKind::Cached(CachedNode {
235+ NodeKind::Text(_) => None,
182− id,
236+ NodeKind::Element(e) => Some(e.flags),
183− _phantom: PhantomData,
237+ NodeKind::Cached(c) => c.flags,
184− }),
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())),
185 }
247 }
186 }
248 }
187}
249}
src/vdom.rs+2−3
@@ -166,7 +166,8 @@ impl Vdom {
166
166
167 // Create a dummy `<div/>` in our container.
167 // Create a dummy `<div/>` in our container.
168 initialize_container(container);
168 initialize_container(container);
169+ let current_root =
169− let current_root = Node::element(&dom_buffers[0], "div", [], [], [], None);
170+ Node::element(&dom_buffers[0], Default::default(), "div", [], [], [], None);
170 let current_root = Some(unsafe { extend_node_lifetime(current_root) });
171 let current_root = Some(unsafe { extend_node_lifetime(current_root) });
171
172
172 let container = container.clone();
173 let container = container.clone();
@@ -424,7 +425,6 @@ impl VdomWeak {
424 /// If you don't want to do more things after the render completes, then use
425 /// If you don't want to do more things after the render completes, then use
425 /// `schedule_render` instead of `render`.
426 /// `schedule_render` instead of `render`.
426 pub fn render(&self) -> impl Future<Item = (), Error = VdomDroppedError> {
427 pub fn render(&self) -> impl Future<Item = (), Error = VdomDroppedError> {
427− debug!("VdomWeak::render: initiating render in new animation frame");
428 futures::future::ok(self.inner.upgrade())
428 futures::future::ok(self.inner.upgrade())
429 .and_then(|inner| inner.ok_or(()))
429 .and_then(|inner| inner.ok_or(()))
430 .map_err(|_| VdomDroppedError {})
430 .map_err(|_| VdomDroppedError {})
@@ -449,7 +449,6 @@ impl VdomWeak {
449 // animation frames.
449 // animation frames.
450 let _ = inner.shared.render_scheduled.take();
450 let _ = inner.shared.render_scheduled.take();
451
451
452− debug!("VdomWeak::render: finished rendering");
453 let r = resolve.call0(&JsValue::null());
452 let r = resolve.call0(&JsValue::null());
454 debug_assert!(r.is_ok());
453 debug_assert!(r.is_ok());
455 }
454 }
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);
19pub mod cached;
19pub mod cached;
20pub mod events;
20pub mod events;
21pub mod js_api;
21pub mod js_api;
22+pub mod keyed;
22pub mod render;
23pub mod render;
23
24
24pub fn window() -> web_sys::Window {
25pub fn window() -> web_sys::Window {