on master help sign in
Merge pull request #59 from fitzgen/keyed-children

Support keyed children

commit2be0c4707916bd5f5c3a41de454bb19fbd11637a authorNick Fitzgerald <fitzgen@gmail.com> dateFri 19 Apr 2019, 13:39 (8 years ago) parent0a993e92c647bf
25 files changed+1428−289
Cargo.lock+101−91 Cargo.toml+10−8 crates/js-api/Cargo.toml+4−4 examples/counter/Cargo.toml+3−3 examples/game-of-life/Cargo.toml+5−5 examples/hello-world/Cargo.toml+3−3 examples/input-form/Cargo.toml+3−3 examples/js-component/Cargo.toml+3−3 examples/moire/Cargo.toml+5−5 examples/sierpinski-triangle/Cargo.toml+5−5 examples/todomvc/Cargo.toml+5−5 js/change-list-interpreter.js+54−7 src/builder.rs+48−2 src/cached.rs+17−10 src/cached_set.rs+23−28 src/change_list/emitter.rs+56 src/change_list/mod.rs+52−2 src/change_list/strings.rs+4−3 src/diff.rs+665−58 src/events.rs+8−17 src/node.rs+53−20 src/vdom.rs+4−6 tests/web/keyed.js+25 tests/web/keyed.rs+270 tests/web/main.rs+2−1
Cargo.lock+101−91
@@ -60,7 +60,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
6060
6161[[package]]
6262name = "bumpalo"
63−version = "2.3.0"
63+version = "2.4.1"
6464source = "registry+https://github.com/rust-lang/crates.io-index"
6565
6666[[package]]
@@ -107,7 +107,7 @@ version = "0.1.6"
107107source = "registry+https://github.com/rust-lang/crates.io-index"
108108dependencies = [
109109 "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
110− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
110+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
111111]
112112
113113[[package]]
@@ -116,26 +116,28 @@ version = "0.1.2"
116116source = "registry+https://github.com/rust-lang/crates.io-index"
117117dependencies = [
118118 "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
119− "web-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
119+ "web-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
120120]
121121
122122[[package]]
123123name = "criterion"
124−version = "0.2.10"
124+version = "0.2.11"
125125source = "registry+https://github.com/rust-lang/crates.io-index"
126126dependencies = [
127127 "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
128128 "cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
129129 "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
130− "criterion-plot 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
130+ "criterion-plot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
131131 "csv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
132132 "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
133133 "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
134+ "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
134135 "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
135136 "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
136137 "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
137138 "rand_xoshiro 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
138139 "rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
140+ "rayon-core 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
139141 "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)",
140142 "serde_derive 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)",
141143 "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -145,7 +147,7 @@ dependencies = [
145147
146148[[package]]
147149name = "criterion-plot"
148−version = "0.3.0"
150+version = "0.3.1"
149151source = "registry+https://github.com/rust-lang/crates.io-index"
150152dependencies = [
151153 "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -205,19 +207,21 @@ dependencies = [
205207name = "dodrio"
206208version = "0.1.0"
207209dependencies = [
208− "bumpalo 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
210+ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
211+ "bumpalo 2.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
209212 "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
210213 "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
211− "criterion 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
214+ "criterion 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
212215 "dodrio-js-api 0.1.0",
213216 "futures 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)",
214217 "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)",
218+ "js-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
216219 "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
217− "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)",
219− "wasm-bindgen-test 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
220− "web-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
220+ "longest-increasing-subsequence 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
221+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
222+ "wasm-bindgen-futures 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
223+ "wasm-bindgen-test 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
224+ "web-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
221225]
222226
223227[[package]]
@@ -228,9 +232,9 @@ dependencies = [
228232 "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
229233 "dodrio 0.1.0",
230234 "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
231− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
232− "wasm-bindgen-test 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
233− "web-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
235+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
236+ "wasm-bindgen-test 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
237+ "web-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
234238]
235239
236240[[package]]
@@ -240,11 +244,11 @@ dependencies = [
240244 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
241245 "dodrio 0.1.0",
242246 "futures 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)",
243− "js-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
244− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
245− "wasm-bindgen-futures 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
246− "wasm-bindgen-test 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
247− "web-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
247+ "js-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
248+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
249+ "wasm-bindgen-futures 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
250+ "wasm-bindgen-test 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
251+ "web-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
248252]
249253
250254[[package]]
@@ -253,9 +257,9 @@ version = "0.1.0"
253257dependencies = [
254258 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
255259 "dodrio 0.1.0",
256− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
257− "wasm-bindgen-test 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
258− "web-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
260+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
261+ "wasm-bindgen-test 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
262+ "web-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
259263]
260264
261265[[package]]
@@ -264,9 +268,9 @@ version = "0.1.0"
264268dependencies = [
265269 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
266270 "dodrio 0.1.0",
267− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
268− "wasm-bindgen-test 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
269− "web-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
271+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
272+ "wasm-bindgen-test 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
273+ "web-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
270274]
271275
272276[[package]]
@@ -275,10 +279,10 @@ version = "0.1.0"
275279dependencies = [
276280 "dodrio 0.1.0",
277281 "futures 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)",
278− "js-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
279− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
280− "wasm-bindgen-futures 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
281− "wasm-bindgen-test 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
282+ "js-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
283+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
284+ "wasm-bindgen-futures 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
285+ "wasm-bindgen-test 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
282286]
283287
284288[[package]]
@@ -288,9 +292,9 @@ dependencies = [
288292 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
289293 "dodrio 0.1.0",
290294 "dodrio-js-api 0.1.0",
291− "js-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
292− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
293− "web-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
295+ "js-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
296+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
297+ "web-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
294298]
295299
296300[[package]]
@@ -302,12 +306,12 @@ dependencies = [
302306 "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
303307 "dodrio 0.1.0",
304308 "futures 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)",
305− "js-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
309+ "js-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
306310 "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
307− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
308− "wasm-bindgen-futures 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
309− "wasm-bindgen-test 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
310− "web-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
311+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
312+ "wasm-bindgen-futures 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
313+ "wasm-bindgen-test 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
314+ "web-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
311315]
312316
313317[[package]]
@@ -317,11 +321,11 @@ dependencies = [
317321 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
318322 "dodrio 0.1.0",
319323 "futures 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)",
320− "js-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
321− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
322− "wasm-bindgen-futures 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
323− "wasm-bindgen-test 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
324− "web-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
324+ "js-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
325+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
326+ "wasm-bindgen-futures 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
327+ "wasm-bindgen-test 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
328+ "web-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
325329]
326330
327331[[package]]
@@ -333,14 +337,14 @@ dependencies = [
333337 "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
334338 "dodrio 0.1.0",
335339 "futures 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)",
336− "js-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
340+ "js-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
337341 "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
338342 "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)",
339343 "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
340− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
341− "wasm-bindgen-futures 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
342− "wasm-bindgen-test 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
343− "web-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
344+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
345+ "wasm-bindgen-futures 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
346+ "wasm-bindgen-test 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
347+ "web-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
344348]
345349
346350[[package]]
@@ -429,10 +433,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
429433
430434[[package]]
431435name = "js-sys"
432−version = "0.3.17"
436+version = "0.3.19"
433437source = "registry+https://github.com/rust-lang/crates.io-index"
434438dependencies = [
435− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
439+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
436440]
437441
438442[[package]]
@@ -453,6 +457,11 @@ dependencies = [
453457 "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
454458]
455459
460+[[package]]
461+name = "longest-increasing-subsequence"
462+version = "0.1.0"
463+source = "registry+https://github.com/rust-lang/crates.io-index"
464+
456465[[package]]
457466name = "memchr"
458467version = "2.2.0"
@@ -778,79 +787,79 @@ dependencies = [
778787
779788[[package]]
780789name = "wasm-bindgen"
781−version = "0.2.40"
790+version = "0.2.42"
782791source = "registry+https://github.com/rust-lang/crates.io-index"
783792dependencies = [
784− "wasm-bindgen-macro 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
793+ "wasm-bindgen-macro 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
785794]
786795
787796[[package]]
788797name = "wasm-bindgen-backend"
789−version = "0.2.40"
798+version = "0.2.42"
790799source = "registry+https://github.com/rust-lang/crates.io-index"
791800dependencies = [
792− "bumpalo 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
801+ "bumpalo 2.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
793802 "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
794803 "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
795804 "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
796805 "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
797806 "syn 0.15.30 (registry+https://github.com/rust-lang/crates.io-index)",
798− "wasm-bindgen-shared 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
807+ "wasm-bindgen-shared 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
799808]
800809
801810[[package]]
802811name = "wasm-bindgen-futures"
803−version = "0.3.17"
812+version = "0.3.19"
804813source = "registry+https://github.com/rust-lang/crates.io-index"
805814dependencies = [
806815 "futures 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)",
807− "js-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
808− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
816+ "js-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
817+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
809818]
810819
811820[[package]]
812821name = "wasm-bindgen-macro"
813−version = "0.2.40"
822+version = "0.2.42"
814823source = "registry+https://github.com/rust-lang/crates.io-index"
815824dependencies = [
816825 "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
817− "wasm-bindgen-macro-support 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
826+ "wasm-bindgen-macro-support 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
818827]
819828
820829[[package]]
821830name = "wasm-bindgen-macro-support"
822−version = "0.2.40"
831+version = "0.2.42"
823832source = "registry+https://github.com/rust-lang/crates.io-index"
824833dependencies = [
825834 "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
826835 "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
827836 "syn 0.15.30 (registry+https://github.com/rust-lang/crates.io-index)",
828− "wasm-bindgen-backend 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
829− "wasm-bindgen-shared 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
837+ "wasm-bindgen-backend 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
838+ "wasm-bindgen-shared 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
830839]
831840
832841[[package]]
833842name = "wasm-bindgen-shared"
834−version = "0.2.40"
843+version = "0.2.42"
835844source = "registry+https://github.com/rust-lang/crates.io-index"
836845
837846[[package]]
838847name = "wasm-bindgen-test"
839−version = "0.2.40"
848+version = "0.2.42"
840849source = "registry+https://github.com/rust-lang/crates.io-index"
841850dependencies = [
842851 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
843852 "futures 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)",
844− "js-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
853+ "js-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
845854 "scoped-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
846− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
847− "wasm-bindgen-futures 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
848− "wasm-bindgen-test-macro 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
855+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
856+ "wasm-bindgen-futures 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
857+ "wasm-bindgen-test-macro 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
849858]
850859
851860[[package]]
852861name = "wasm-bindgen-test-macro"
853−version = "0.2.40"
862+version = "0.2.42"
854863source = "registry+https://github.com/rust-lang/crates.io-index"
855864dependencies = [
856865 "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -859,7 +868,7 @@ dependencies = [
859868
860869[[package]]
861870name = "wasm-bindgen-webidl"
862−version = "0.2.40"
871+version = "0.2.42"
863872source = "registry+https://github.com/rust-lang/crates.io-index"
864873dependencies = [
865874 "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -868,21 +877,21 @@ dependencies = [
868877 "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
869878 "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
870879 "syn 0.15.30 (registry+https://github.com/rust-lang/crates.io-index)",
871− "wasm-bindgen-backend 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
880+ "wasm-bindgen-backend 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
872881 "weedle 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
873882]
874883
875884[[package]]
876885name = "web-sys"
877−version = "0.3.17"
886+version = "0.3.19"
878887source = "registry+https://github.com/rust-lang/crates.io-index"
879888dependencies = [
880889 "env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
881890 "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
882− "js-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
891+ "js-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
883892 "sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
884− "wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
885− "wasm-bindgen-webidl 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
893+ "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
894+ "wasm-bindgen-webidl 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
886895]
887896
888897[[package]]
@@ -937,7 +946,7 @@ dependencies = [
937946"checksum backtrace 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "f106c02a3604afcdc0df5d36cc47b44b55917dbaf3d808f71c163a0ddba64637"
938947"checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6"
939948"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
940−"checksum bumpalo 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f6a6bc2ba935b1e2f810787ceba8dfe86cbc164cee55925cae715541186673c4"
949+"checksum bumpalo 2.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4639720be048090544634e0402490838995ccdc9d2fe648f528f30d3c33ae71f"
941950"checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb"
942951"checksum cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "926013f2860c46252efceabb19f4a6b308197505082c609025aa6706c011d427"
943952"checksum cc 1.0.34 (registry+https://github.com/rust-lang/crates.io-index)" = "30f813bf45048a18eda9190fd3c6b78644146056740c43172a5a3699118588fd"
@@ -946,8 +955,8 @@ dependencies = [
946955"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
947956"checksum console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211"
948957"checksum console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1e7871d2947441b0fdd8e2bd1ce2a2f75304f896582c0d572162d48290683c48"
949−"checksum criterion 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "1c6e5ee5b9652d4f851418c448af105642e1f99e9a2741a8ff45c0d2c911b1e0"
950−"checksum criterion-plot 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4107e4a5abb94267e0149922b8ff49dc70a87cc202820fdbfc0d3e1edbdc4b16"
958+"checksum criterion 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "0363053954f3e679645fc443321ca128b7b950a6fe288cf5f9335cc22ee58394"
959+"checksum criterion-plot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76f9212ddf2f4a9eb2d401635190600656a1f88a932ef53d06e7fa4c7e02fb8e"
951960"checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3"
952961"checksum crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "927121f5407de9956180ff5e936fe3cf4324279280001cd56b669d28ee7e9150"
953962"checksum crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2760899e32a1d58d5abb31129f8fae5de75220bc2176e77ff7c627ae45c918d9"
@@ -964,10 +973,11 @@ dependencies = [
964973"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
965974"checksum itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5b8467d9c1cebe26feb08c640139247fac215782d35371ade9a2136ed6085358"
966975"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b"
967−"checksum js-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "d4bda84b98977341bb6ba1086ecbd9eab8bc929de0f2923c118baf76c21dd0c8"
976+"checksum js-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)" = "3c994fd445b81741d77f6bcd227d6ed645b95b35a2ecfd2050767450ff1c0b6d"
968977"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
969978"checksum libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "bedcc7a809076656486ffe045abeeac163da1b558e963a31e29fbfbeba916917"
970979"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
980+"checksum longest-increasing-subsequence 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86"
971981"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
972982"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3"
973983"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
@@ -1011,16 +1021,16 @@ dependencies = [
10111021"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737"
10121022"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
10131023"checksum walkdir 2.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "9d9d7ed3431229a144296213105a390676cc49c9b6a72bd19f3176c98e129fa1"
1014−"checksum wasm-bindgen 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "9742fc4860f47bede1090a5e4b0cfc33afcd70cfdf45dd28f2cfb02d4662b0dd"
1015−"checksum wasm-bindgen-backend 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c6d7f35ecbb4180513cdb9b298543321bcb278670730415cbb3205ff2c66a477"
1016−"checksum wasm-bindgen-futures 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "67bfbec85e48e8915b6acfa1c4d90b770ce033c96dc2067688f3bccd00d3b9e1"
1017−"checksum wasm-bindgen-macro 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "e3c86b06bcd28e92e87d2c2ad208889b2f69ea33f79810b91ef660cc3de65a4c"
1018−"checksum wasm-bindgen-macro-support 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "81d7338dd8c67e193d8ef18e5802dc03d8710456baa792c1c2e66847e57fd389"
1019−"checksum wasm-bindgen-shared 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "0d57c3b66f2f3e4d96b50f49b7b7e2f4cfcddc88b15744433c98c5c105b26672"
1020−"checksum wasm-bindgen-test 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "85bbc3ad78a8d59a63a02d59345e8da9a3e0c1617f79e77fd1c3da6e047db63e"
1021−"checksum wasm-bindgen-test-macro 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "114dfdf01bd3659dd79db05480fbd2751759d1c79cd9edc22f2fe611c866e932"
1022−"checksum wasm-bindgen-webidl 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "fc7052a577eefce17a439edfce5c2eedc8432fe7457bbe949624cdf946233fa7"
1023−"checksum web-sys 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "72d02003924c77af871ad74216f828b6e32af776cbb3f5dab3dcde2f2713c012"
1024+"checksum wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "ffde3534e5fa6fd936e3260cd62cd644b8656320e369388f9303c955895e35d4"
1025+"checksum wasm-bindgen-backend 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "40c0543374a7ae881cdc5d32d19de28d1d1929e92263ffa7e31712cc2d53f9f1"
1026+"checksum wasm-bindgen-futures 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)" = "0ad171fc1f6e43f97d155d27f4ee5657bd8aa5cce7c497ef3a0a0c5b44618b2d"
1027+"checksum wasm-bindgen-macro 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "f914c94c2c5f4c9364510ca2429e59c92157ec89429243bcc245e983db990a71"
1028+"checksum wasm-bindgen-macro-support 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "9168c413491e4233db7b6884f09a43beb00c14d11d947ffd165242daa48a2385"
1029+"checksum wasm-bindgen-shared 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "326c32126e1a157b6ced7400061a84ac5b11182b2cda6edad7314eb3ae9ac9fe"
1030+"checksum wasm-bindgen-test 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "8aed4ef1aa56f2ef4aff593c516de6a3e48711c703b3f7415c9151a7c9babd17"
1031+"checksum wasm-bindgen-test-macro 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "eb477c50f3c9a9ecd99c78fb9bd3b6599d414b918d5f72165b12c53738011ae1"
1032+"checksum wasm-bindgen-webidl 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "613dbf4d7d3bf10aeb212b35de14a8ef07222c26526d4f931061a83fc9e2a851"
1033+"checksum web-sys 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)" = "24129e4be2281109b3e15a328d3d7f233ee232a5405f75ba1e9bb59a25ebc4d4"
10241034"checksum weedle 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26a4c67f132386d965390b8a734d5d10adbcd30eb5cc74bd9229af8b83f10044"
10251035"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770"
10261036"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
Cargo.toml+10−8
@@ -34,17 +34,19 @@ required-features = ["xxx-unstable-internal-use-only"]
3434xxx-unstable-internal-use-only = []
3535
3636[dependencies]
37−bumpalo = "2.3.0"
37+bumpalo = "2.4.1"
3838cfg-if = "0.1.7"
3939futures = "0.1.26"
40−wasm-bindgen = "0.2.40"
41−wasm-bindgen-futures = "0.3.17"
42−js-sys = "0.3.17"
40+wasm-bindgen = "0.2.42"
41+wasm-bindgen-futures = "0.3.19"
42+js-sys = "0.3.19"
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]
47−version = "0.3.17"
49+version = "0.3.19"
4850features = [
4951 "console",
5052 "Document",
@@ -58,10 +60,10 @@ features = [
5860console_log = "0.1.2"
5961dodrio-js-api = { version = "=0.1.0", path = "./crates/js-api" }
6062log = "0.4.6"
61−wasm-bindgen-test = "0.2.40"
63+wasm-bindgen-test = "0.2.42"
6264
6365[dev-dependencies.web-sys]
64−version = "0.3.17"
66+version = "0.3.19"
6567features = [
6668 "Attr",
6769 "EventTarget",
@@ -71,7 +73,7 @@ features = [
7173]
7274
7375[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
74−criterion = "0.2.10"
76+criterion = "0.2.11"
7577
7678[profile.bench]
7779debug = true
crates/js-api/Cargo.toml+4−4
@@ -14,10 +14,10 @@ travis-ci = { repository = "fitzgen/dodrio" }
1414
1515[dependencies]
1616dodrio = { version = "=0.1.0", path = "../.." }
17−wasm-bindgen = "0.2.40"
18−js-sys = "0.3.17"
19−wasm-bindgen-futures = "0.3.17"
17+wasm-bindgen = "0.2.42"
18+js-sys = "0.3.19"
19+wasm-bindgen-futures = "0.3.19"
2020futures = "0.1.26"
2121
2222[dev-dependencies]
23−wasm-bindgen-test = "0.2.40"
23+wasm-bindgen-test = "0.2.42"
examples/counter/Cargo.toml+3−3
@@ -14,10 +14,10 @@ console_error_panic_hook = "0.1.6"
1414console_log = "0.1.2"
1515dodrio = { path = "../.." }
1616log = "0.4.6"
17−wasm-bindgen = "0.2.40"
17+wasm-bindgen = "0.2.42"
1818
1919[dependencies.web-sys]
20−version = "0.3.17"
20+version = "0.3.19"
2121features = [
2222 "console",
2323 "Document",
@@ -30,4 +30,4 @@ features = [
3030]
3131
3232[dev-dependencies]
33−wasm-bindgen-test = "0.2.40"
33+wasm-bindgen-test = "0.2.42"
examples/game-of-life/Cargo.toml+5−5
@@ -11,14 +11,14 @@ crate-type = ["cdylib"]
1111
1212[dependencies]
1313dodrio = { path = "../.." }
14−wasm-bindgen = "0.2.40"
14+wasm-bindgen = "0.2.42"
1515console_error_panic_hook = "0.1.6"
16−js-sys = "0.3.17"
17−wasm-bindgen-futures = "0.3.17"
16+js-sys = "0.3.19"
17+wasm-bindgen-futures = "0.3.19"
1818futures = "0.1.26"
1919
2020[dependencies.web-sys]
21−version = "0.3.17"
21+version = "0.3.19"
2222features = [
2323 "Document",
2424 "HtmlElement",
@@ -27,4 +27,4 @@ features = [
2727]
2828
2929[dev-dependencies]
30−wasm-bindgen-test = "0.2.40"
30+wasm-bindgen-test = "0.2.42"
examples/hello-world/Cargo.toml+3−3
@@ -11,11 +11,11 @@ crate-type = ["cdylib"]
1111
1212[dependencies]
1313dodrio = { path = "../.." }
14−wasm-bindgen = "0.2.40"
14+wasm-bindgen = "0.2.42"
1515console_error_panic_hook = "0.1.6"
1616
1717[dependencies.web-sys]
18−version = "0.3.17"
18+version = "0.3.19"
1919features = [
2020 "Document",
2121 "HtmlElement",
@@ -24,4 +24,4 @@ features = [
2424]
2525
2626[dev-dependencies]
27−wasm-bindgen-test = "0.2.40"
27+wasm-bindgen-test = "0.2.42"
examples/input-form/Cargo.toml+3−3
@@ -11,11 +11,11 @@ crate-type = ["cdylib"]
1111
1212[dependencies]
1313dodrio = { path = "../.." }
14−wasm-bindgen = "0.2.40"
14+wasm-bindgen = "0.2.42"
1515console_error_panic_hook = "0.1.6"
1616
1717[dependencies.web-sys]
18−version = "0.3.17"
18+version = "0.3.19"
1919features = [
2020 "console",
2121 "Document",
@@ -29,4 +29,4 @@ features = [
2929]
3030
3131[dev-dependencies]
32−wasm-bindgen-test = "0.2.40"
32+wasm-bindgen-test = "0.2.42"
examples/js-component/Cargo.toml+3−3
@@ -12,12 +12,12 @@ crate-type = ["cdylib"]
1212[dependencies]
1313dodrio = { path = "../.." }
1414dodrio-js-api = { path = "../../crates/js-api" }
15−wasm-bindgen = "0.2.40"
15+wasm-bindgen = "0.2.42"
1616console_error_panic_hook = "0.1.6"
17−js-sys = "0.3.17"
17+js-sys = "0.3.19"
1818
1919[dependencies.web-sys]
20−version = "0.3.17"
20+version = "0.3.19"
2121features = [
2222 "Document",
2323 "HtmlElement",
examples/moire/Cargo.toml+5−5
@@ -12,16 +12,16 @@ cfg-if = "0.1.7"
1212dodrio = { path = "../.." }
1313futures = "0.1.26"
1414log = "0.4.6"
15−wasm-bindgen = "0.2.40"
16−wasm-bindgen-futures = "0.3.17"
15+wasm-bindgen = "0.2.42"
16+wasm-bindgen-futures = "0.3.19"
1717
1818# Optional dependencies for logging.
1919console_error_panic_hook = { optional = true, version = "0.1.6" }
2020console_log = { optional = true, version = "0.1.2" }
21−js-sys = "0.3.17"
21+js-sys = "0.3.19"
2222
2323[dependencies.web-sys]
24−version = "0.3.17"
24+version = "0.3.19"
2525features = [
2626 "Document",
2727 "Element",
@@ -35,7 +35,7 @@ features = [
3535]
3636
3737[dev-dependencies]
38−wasm-bindgen-test = "0.2.40"
38+wasm-bindgen-test = "0.2.42"
3939
4040[features]
4141logging = [
examples/sierpinski-triangle/Cargo.toml+5−5
@@ -9,14 +9,14 @@ crate-type = ["cdylib"]
99
1010[dependencies]
1111dodrio = { path = "../.." }
12−wasm-bindgen = "0.2.40"
12+wasm-bindgen = "0.2.42"
1313console_error_panic_hook = "0.1.6"
14−js-sys = "0.3.17"
15−wasm-bindgen-futures = "0.3.17"
14+js-sys = "0.3.19"
15+wasm-bindgen-futures = "0.3.19"
1616futures = "0.1.26"
1717
1818[dependencies.web-sys]
19−version = "0.3.17"
19+version = "0.3.19"
2020features = [
2121 "Document",
2222 "HtmlElement",
@@ -26,4 +26,4 @@ features = [
2626]
2727
2828[dev-dependencies]
29−wasm-bindgen-test = "0.2.40"
29+wasm-bindgen-test = "0.2.42"
examples/todomvc/Cargo.toml+5−5
@@ -8,11 +8,11 @@ version = "0.1.0"
88cfg-if = "0.1.7"
99dodrio = { path = "../.." }
1010futures = "0.1.26"
11−js-sys = "0.3.17"
11+js-sys = "0.3.19"
1212serde = { features = ["derive"], version = "1.0.90" }
1313serde_json = "1.0.39"
14−wasm-bindgen = "0.2.40"
15−wasm-bindgen-futures = "0.3.17"
14+wasm-bindgen = "0.2.42"
15+wasm-bindgen-futures = "0.3.19"
1616
1717# Optional dependencies for logging.
1818console_error_panic_hook = { optional = true, version = "0.1.6" }
@@ -20,7 +20,7 @@ console_log = { optional = true, version = "0.1.2" }
2020log = { optional = true, version = "0.4.6" }
2121
2222[dependencies.web-sys]
23−version = "0.3.17"
23+version = "0.3.19"
2424features = [
2525 "Document",
2626 "Event",
@@ -35,7 +35,7 @@ features = [
3535]
3636
3737[dev-dependencies]
38−wasm-bindgen-test = "0.2.40"
38+wasm-bindgen-test = "0.2.42"
3939
4040[features]
4141logging = [
js/change-list-interpreter.js+54−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) {
@@ -242,6 +288,7 @@ export class ChangeListInterpreter {
242288
243289 this.ranges.length = 0;
244290 this.stack.length = 0;
291+ this.temporaries.length = 0;
245292 }
246293
247294 applyChangeRange(mem8, mem32, start, len) {
src/builder.rs+48−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::NodeKey, 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+ key: NodeKey,
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+ key: NodeKey::NONE,
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+ key: self.key,
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+ key: self.key,
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+ key: self.key,
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+ key: self.key,
215221 tag_name: self.tag_name,
216222 listeners: self.listeners,
217223 attributes: self.attributes,
@@ -220,6 +226,45 @@ where
220226 }
221227 }
222228
229+ /// Set this element's key.
230+ ///
231+ /// When diffing sets of siblings, if an old sibling and new sibling share a
232+ /// key, then they will always reuse the same physical DOM node. This is
233+ /// important when using CSS animations, web components, third party JS, or
234+ /// anything else that makes the diffing implementation observable.
235+ ///
236+ /// Do not use keys if such a scenario does not apply. Keyed diffing is
237+ /// generally more expensive than not, since it is putting greater
238+ /// constraints on the diffing algorithm.
239+ ///
240+ /// # Invariants You Must Uphold
241+ ///
242+ /// The key may not be `u32::MAX`, which is a reserved key value.
243+ ///
244+ /// Keys must be unique among siblings.
245+ ///
246+ /// All sibling nodes must be keyed, or they must all not be keyed. You may
247+ /// not mix keyed and unkeyed siblings.
248+ ///
249+ /// # Example
250+ ///
251+ /// ```no_run
252+ /// use dodrio::{builder::*, bumpalo::Bump};
253+ ///
254+ /// let b = Bump::new();
255+ ///
256+ /// let my_li = li(&b)
257+ /// .key(1337)
258+ /// .finish();
259+ /// ```
260+ #[inline]
261+ pub fn key(mut self, key: u32) -> Self {
262+ use std::u32;
263+ debug_assert!(key != u32::MAX);
264+ self.key = NodeKey(key);
265+ self
266+ }
267+
223268 /// Create the virtual DOM node described by this builder.
224269 ///
225270 /// # Example
@@ -239,6 +284,7 @@ where
239284 pub fn finish(self) -> Node<'a> {
240285 Node::element(
241286 self.bump,
287+ self.key,
242288 self.tag_name,
243289 self.listeners,
244290 self.attributes,
src/cached.rs+17−10
@@ -1,5 +1,6 @@
11use crate::{
2− cached_set::{CacheId, CachedSet},
2+ cached_set::CachedSet,
3+ node::{CachedNode, NodeKey},
34 Node, Render, RenderContext,
45};
56use std::cell::Cell;
@@ -10,7 +11,7 @@ use std::ops::{Deref, DerefMut};
1011#[derive(Clone, Debug)]
1112pub struct Cached<R> {
1213 inner: R,
13− cached: Cell<Option<CacheId>>,
14+ cached: Cell<Option<CachedNode>>,
1415}
1516
1617impl<R> Cached<R> {
@@ -112,7 +113,7 @@ where
112113 R: Render,
113114{
114115 fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
115− let id = match self.cached.get() {
116+ let cached = match self.cached.get() {
116117 // This does-the-cache-contain-this-id check is necessary because
117118 // the same `Cached<R>` instance can be rendered into vdom A, which
118119 // will save the results into A's cached set and yield id X. Then,
@@ -140,21 +141,27 @@ where
140141 //
141142 // This is all do-able but is a bit more book keeping than we really
142143 // want to do unless it is well motivated.
143− Some(id)
144+ Some(cached)
144145 if {
145146 let cached_set = cx.cached_set.borrow();
146− cached_set.contains(id)
147+ cached_set.contains(cached.id)
147148 } =>
148149 {
149− id
150+ cached
150151 }
151152 _ => {
152− let id = CachedSet::insert(cx, |nested_cx| self.inner.render(nested_cx));
153− self.cached.set(Some(id));
154− id
153+ let mut key = NodeKey::NONE;
154+ let id = CachedSet::insert(cx, |nested_cx| {
155+ let node = self.inner.render(nested_cx);
156+ key = node.key();
157+ node
158+ });
159+ let cached = CachedNode { id, key };
160+ self.cached.set(Some(cached));
161+ cached
155162 }
156163 };
157164
158− Node::cached(id)
165+ cached.into()
159166 }
160167}
src/cached_set.rs+23−28
@@ -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 }
@@ -76,33 +75,31 @@ impl CachedSet {
7675 // node has.
7776 fn trace(&self, node: &Node) -> FxHashSet<CacheId> {
7877 let mut edges = FxHashSet::default();
78+ self.trace_recursive(&mut edges, node);
79+ edges
80+ }
7981
80− let bump = Bump::new();
81− let mut stack = bumpalo::collections::Vec::with_capacity_in(64, &bump);
82− stack.push(node);
83−
84− while let Some(node) = stack.pop() {
85− match &node.kind {
86− NodeKind::Text(_) => continue,
87− NodeKind::Cached(c) => {
88− debug_assert!(self.items.contains_key(&c.id));
89− edges.insert(c.id);
90− edges.extend(
91− self.items
92− .get(&c.id)
93− .expect_throw("CachedSet::trace: should have c.id in cached set")
94− .edges
95− .iter()
96− .cloned(),
97− );
98− }
99− NodeKind::Element(el) => {
100− stack.extend(el.children);
82+ fn trace_recursive(&self, edges: &mut FxHashSet<CacheId>, node: &Node) {
83+ match &node.kind {
84+ NodeKind::Text(_) => return,
85+ NodeKind::Cached(c) => {
86+ debug_assert!(self.items.contains_key(&c.id));
87+ edges.insert(c.id);
88+ edges.extend(
89+ self.items
90+ .get(&c.id)
91+ .expect_throw("CachedSet::trace: should have c.id in cached set")
92+ .edges
93+ .iter()
94+ .cloned(),
95+ );
96+ }
97+ NodeKind::Element(el) => {
98+ for child in el.children {
99+ self.trace_recursive(edges, child);
101100 }
102101 }
103102 }
104−
105− edges
106103 }
107104
108105 fn next_id(&mut self) -> CacheId {
@@ -129,7 +126,6 @@ impl CachedSet {
129126
130127 let mut set = set.borrow_mut();
131128 let id = set.next_id();
132− debug!("CachedSet::insert: id = {:?}; entry = {:?}", id, entry);
133129 set.items.insert(id, entry);
134130 id
135131 }
@@ -140,8 +136,7 @@ impl CachedSet {
140136 }
141137
142138 /// Get the node for the given cache id.
143− pub fn get(&self, mut id: CacheId) -> Node {
144− debug!("CachedSet::get: id = {:?}", id);
139+ pub fn get(&self, mut id: CacheId) -> &Node {
145140 loop {
146141 let entry = self
147142 .items
@@ -152,7 +147,7 @@ impl CachedSet {
152147 id = c.id;
153148 continue;
154149 } else {
155− return node.clone();
150+ return node;
156151 }
157152 }
158153 }
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+665−58
@@ -4,15 +4,26 @@ use crate::{
44 events::EventsRegistry,
55 node::{Attribute, ElementNode, 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::u32;
10+use wasm_bindgen::UnwrapThrowExt;
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+// change list stack:
17+//
18+// [... node]
19+//
20+// The change list stack is in the same state when this function exits.
1021pub(crate) fn diff(
1122 cached_set: &CachedSet,
1223 change_list: &mut ChangeListBuilder,
1324 registry: &mut EventsRegistry,
14− old: Node,
15− new: Node,
25+ old: &Node,
26+ new: &Node,
1627 cached_roots: &mut FxHashSet<CacheId>,
1728) {
1829 match (&new.kind, &old.kind) {
@@ -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+ key: _,
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+ key: _,
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();
@@ -113,14 +120,19 @@ pub(crate) fn diff(
113120 }
114121}
115122
123+// Diff event listeners between `old` and `new`.
124+//
125+// The listeners' node must be on top of the change list stack:
126+//
127+// [... node]
128+//
129+// The change list stack is left unchanged.
116130fn diff_listeners(
117131 change_list: &mut ChangeListBuilder,
118132 registry: &mut EventsRegistry,
119133 old: &[Listener],
120134 new: &[Listener],
121135) {
122− debug!(" updating event listeners");
123−
124136 'outer1: for new_l in new {
125137 unsafe {
126138 // Safety relies on removing `new_l` from the registry manually at
@@ -151,9 +163,14 @@ fn diff_listeners(
151163 }
152164}
153165
166+// Diff a node's attributes.
167+//
168+// The attributes' node must be on top of the change list stack:
169+//
170+// [... node]
171+//
172+// The change list stack is left unchanged.
154173fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new: &[Attribute]) {
155− debug!(" updating attributes");
156−
157174 // Do O(n^2) passes to add/update and remove attributes, since
158175 // there are almost always very few attributes.
159176 'outer: for new_attr in new {
@@ -182,6 +199,14 @@ fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new:
182199 }
183200}
184201
202+// Diff the given set of old and new children.
203+//
204+// The parent must be on top of the change list stack when this function is
205+// entered:
206+//
207+// [... parent]
208+//
209+// the change list stack is in the same state when this function returns.
185210fn diff_children(
186211 cached_set: &CachedSet,
187212 change_list: &mut ChangeListBuilder,
@@ -190,83 +215,671 @@ fn diff_children(
190215 new: &[Node],
191216 cached_roots: &mut FxHashSet<CacheId>,
192217) {
193− debug!(" updating children shared by old and new");
218+ if new.is_empty() {
219+ if !old.is_empty() {
220+ remove_all_children(change_list, registry, old);
221+ }
222+ return;
223+ }
224+
225+ if old.is_empty() {
226+ create_and_append_children(cached_set, change_list, registry, new, cached_roots);
227+ return;
228+ }
229+
230+ let new_is_keyed = new[0].key().is_some();
231+ let old_is_keyed = old[0].key().is_some();
232+
233+ debug_assert!(
234+ new.iter().all(|n| n.key().is_some() == new_is_keyed),
235+ "all siblings must be keyed or all siblings must be non-keyed"
236+ );
237+ debug_assert!(
238+ old.iter().all(|o| o.key().is_some() == old_is_keyed),
239+ "all siblings must be keyed or all siblings must be non-keyed"
240+ );
241+
242+ if new_is_keyed && old_is_keyed {
243+ let t = change_list.next_temporary();
244+ diff_keyed_children(cached_set, change_list, registry, old, new, cached_roots);
245+ change_list.set_next_temporary(t);
246+ } else {
247+ diff_non_keyed_children(cached_set, change_list, registry, old, new, cached_roots);
248+ }
249+}
250+
251+// Diffing "keyed" children.
252+//
253+// With keyed children, we care about whether we delete, move, or create nodes
254+// versus mutate existing nodes in place. Presumably there is some sort of CSS
255+// transition animation that makes the virtual DOM diffing algorithm
256+// observable. By specifying keys for nodes, we know which virtual DOM nodes
257+// must reuse (or not reuse) the same physical DOM nodes.
258+//
259+// This is loosely based on Inferno's keyed patching implementation. However, we
260+// have to modify the algorithm since we are compiling the diff down into change
261+// list instructions that will be executed later, rather than applying the
262+// changes to the DOM directly as we compare virtual DOMs.
263+//
264+// https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739
265+//
266+// When entering this function, the parent must be on top of the change list
267+// stack:
268+//
269+// [... parent]
270+//
271+// Upon exiting, the change list stack is in the same state.
272+fn diff_keyed_children(
273+ cached_set: &CachedSet,
274+ change_list: &mut ChangeListBuilder,
275+ registry: &mut EventsRegistry,
276+ old: &[Node],
277+ new: &[Node],
278+ cached_roots: &mut FxHashSet<CacheId>,
279+) {
280+ if cfg!(debug_assertions) {
281+ let mut keys = FxHashSet::default();
282+ let mut assert_unique_keys = |children: &[Node]| {
283+ keys.clear();
284+ for child in children {
285+ let key = child.key();
286+ debug_assert!(
287+ key.is_some(),
288+ "if any sibling is keyed, all siblings must be keyed"
289+ );
290+ keys.insert(key);
291+ }
292+ debug_assert_eq!(
293+ children.len(),
294+ keys.len(),
295+ "keyed siblings must each have a unique key"
296+ );
297+ };
298+ assert_unique_keys(old);
299+ assert_unique_keys(new);
300+ }
194301
195− let num_children_to_diff = cmp::min(new.len(), old.len());
196− let mut new_children = new.iter();
197− let mut old_children = old.iter();
302+ // First up, we diff all the nodes with the same key at the beginning of the
303+ // children.
304+ //
305+ // `shared_prefix_count` is the count of how many nodes at the start of
306+ // `new` and `old` share the same keys.
307+ let shared_prefix_count =
308+ match diff_keyed_prefix(cached_set, change_list, registry, old, new, cached_roots) {
309+ KeyedPrefixResult::Finished => return,
310+ KeyedPrefixResult::MoreWorkToDo(count) => count,
311+ };
312+
313+ // Next, we find out how many of the nodes at the end of the children have
314+ // the same key. We do _not_ diff them yet, since we want to emit the change
315+ // list instructions such that they can be applied in a single pass over the
316+ // DOM. Instead, we just save this information for later.
317+ //
318+ // `shared_suffix_count` is the count of how many nodes at the end of `new`
319+ // and `old` share the same keys.
320+ let shared_suffix_count = old[shared_prefix_count..]
321+ .iter()
322+ .rev()
323+ .zip(new[shared_prefix_count..].iter().rev())
324+ .take_while(|&(old, new)| old.key() == new.key())
325+ .count();
326+
327+ let old_shared_suffix_start = old.len() - shared_suffix_count;
328+ let new_shared_suffix_start = new.len() - shared_suffix_count;
329+
330+ // Ok, we now hopefully have a smaller range of children in the middle
331+ // within which to re-order nodes with the same keys, remove old nodes with
332+ // now-unused keys, and create new nodes with fresh keys.
333+ diff_keyed_middle(
334+ cached_set,
335+ change_list,
336+ registry,
337+ &old[shared_prefix_count..old_shared_suffix_start],
338+ &new[shared_prefix_count..new_shared_suffix_start],
339+ cached_roots,
340+ shared_prefix_count,
341+ shared_suffix_count,
342+ old_shared_suffix_start,
343+ );
344+
345+ // Finally, diff the nodes at the end of `old` and `new` that share keys.
346+ let old_suffix = &old[old_shared_suffix_start..];
347+ let new_suffix = &new[new_shared_suffix_start..];
348+ debug_assert_eq!(old_suffix.len(), new_suffix.len());
349+ if !old_suffix.is_empty() {
350+ diff_keyed_suffix(
351+ cached_set,
352+ change_list,
353+ registry,
354+ old_suffix,
355+ new_suffix,
356+ cached_roots,
357+ new_shared_suffix_start,
358+ );
359+ }
360+}
361+
362+enum KeyedPrefixResult {
363+ // Fast path: we finished diffing all the children just by looking at the
364+ // prefix of shared keys!
365+ Finished,
366+ // There is more diffing work to do. Here is a count of how many children at
367+ // the beginning of `new` and `old` we already processed.
368+ MoreWorkToDo(usize),
369+}
370+
371+// Diff the prefix of children in `new` and `old` that share the same keys in
372+// the same order.
373+//
374+// Upon entry of this function, the change list stack must be:
375+//
376+// [... parent]
377+//
378+// Upon exit, the change list stack is the same.
379+fn diff_keyed_prefix(
380+ cached_set: &CachedSet,
381+ change_list: &mut ChangeListBuilder,
382+ registry: &mut EventsRegistry,
383+ old: &[Node],
384+ new: &[Node],
385+ cached_roots: &mut FxHashSet<CacheId>,
386+) -> KeyedPrefixResult {
198387 let mut pushed = false;
388+ let mut shared_prefix_count = 0;
199389
200− for (i, (new_child, old_child)) in new_children
201− .by_ref()
202− .zip(old_children.by_ref())
203− .take(num_children_to_diff)
204− .enumerate()
205− {
206− if i == 0 {
390+ for (old, new) in old.iter().zip(new.iter()) {
391+ if old.key() != new.key() {
392+ break;
393+ }
394+
395+ if pushed {
396+ debug_assert!(shared_prefix_count > 0);
397+ change_list.pop_push_next_sibling();
398+ } else {
399+ debug_assert_eq!(shared_prefix_count, 0);
207400 change_list.push_first_child();
208401 pushed = true;
209− } else {
210− debug_assert!(pushed);
211− change_list.pop_push_next_sibling();
212402 }
213403
214− diff(
404+ diff(cached_set, change_list, registry, old, new, cached_roots);
405+ shared_prefix_count += 1;
406+
407+ // At the end of the loop, the change list stack looks like
408+ //
409+ // [... parent child_we_just_diffed]
410+ debug_assert!(pushed);
411+ }
412+
413+ // If that was all of the old children, then create and append the remaining
414+ // new children and we're finished.
415+ if shared_prefix_count == old.len() {
416+ debug_assert!(
417+ pushed,
418+ "we handle the case of empty children before calling this method, so
419+ `shared_prefix_count` must be greater than zero, and therefore we
420+ must have pushed in the above loop"
421+ );
422+ change_list.pop();
423+ create_and_append_children(
215424 cached_set,
216425 change_list,
217426 registry,
218− old_child.clone(),
219− new_child.clone(),
427+ &new[shared_prefix_count..],
220428 cached_roots,
221429 );
430+ return KeyedPrefixResult::Finished;
222431 }
223432
224− if old_children.next().is_some() {
225− debug!(" removing extra old children");
226− debug_assert!(new_children.next().is_none());
227− if pushed {
433+ // And if that was all of the new children, then remove all of the remaining
434+ // old children and we're finished.
435+ if shared_prefix_count == new.len() {
436+ // Same as above.
437+ debug_assert!(pushed);
438+ change_list.pop_push_next_sibling();
439+ change_list.remove_self_and_next_siblings();
440+ return KeyedPrefixResult::Finished;
441+ }
442+
443+ if pushed {
444+ debug_assert!(shared_prefix_count > 0);
445+ change_list.pop();
446+ }
447+ KeyedPrefixResult::MoreWorkToDo(shared_prefix_count)
448+}
449+
450+// The most-general, expensive code path for keyed children diffing.
451+//
452+// We find the longest subsequence within `old` of children that are relatively
453+// ordered the same way in `new` (via finding a longest-increasing-subsequence
454+// of the old child's index within `new`). The children that are elements of
455+// this subsequence will remain in place, minimizing the number of DOM moves we
456+// will have to do.
457+//
458+// Upon entry to this function, the change list stack must be:
459+//
460+// [... parent]
461+//
462+// Upon exit from this function, it will be restored to that same state.
463+fn diff_keyed_middle(
464+ cached_set: &CachedSet,
465+ change_list: &mut ChangeListBuilder,
466+ registry: &mut EventsRegistry,
467+ old: &[Node],
468+ mut new: &[Node],
469+ cached_roots: &mut FxHashSet<CacheId>,
470+ shared_prefix_count: usize,
471+ shared_suffix_count: usize,
472+ old_shared_suffix_start: usize,
473+) {
474+ // Should have already diffed the shared-key prefixes and suffixes.
475+ debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key()));
476+ debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key()));
477+
478+ // The algorithm below relies upon using `u32::MAX` as a sentinel
479+ // value, so if we have that many new nodes, it won't work. This
480+ // check is a bit academic (hence only enabled in debug), since
481+ // wasm32 doesn't have enough address space to hold that many nodes
482+ // in memory.
483+ debug_assert!(new.len() < u32::MAX as usize);
484+
485+ // Map from each `old` node's key to its index within `old`.
486+ let mut old_key_to_old_index = FxHashMap::default();
487+ old_key_to_old_index.reserve(old.len());
488+ old_key_to_old_index.extend(old.iter().enumerate().map(|(i, o)| (o.key(), i)));
489+
490+ // The set of shared keys between `new` and `old`.
491+ let mut shared_keys = FxHashSet::default();
492+ // Map from each index in `new` to the index of the node in `old` that
493+ // has the same key.
494+ let mut new_index_to_old_index = Vec::with_capacity(new.len());
495+ new_index_to_old_index.extend(new.iter().map(|n| {
496+ let key = n.key();
497+ if let Some(&i) = old_key_to_old_index.get(&key) {
498+ shared_keys.insert(key);
499+ i
500+ } else {
501+ u32::MAX as usize
502+ }
503+ }));
504+
505+ // If none of the old keys are reused by the new children, then we
506+ // remove all the remaining old children and create the new children
507+ // afresh.
508+ if shared_suffix_count == 0 && shared_keys.is_empty() {
509+ if shared_prefix_count == 0 {
510+ remove_all_children(change_list, registry, old);
511+ } else {
228512 change_list.pop_push_next_sibling();
513+ remove_self_and_next_siblings(change_list, registry, &old[shared_prefix_count..]);
514+ }
515+ create_and_append_children(cached_set, change_list, registry, new, cached_roots);
516+ return;
517+ }
518+
519+ // Save each of the old children whose keys are reused in the new
520+ // children.
521+ let mut old_index_to_temp = vec![u32::MAX; old.len()];
522+ let mut start = 0;
523+ loop {
524+ let end = (start..old.len())
525+ .find(|&i| {
526+ let key = old[i].key();
527+ !shared_keys.contains(&key)
528+ })
529+ .unwrap_or(old.len());
530+
531+ if end - start > 0 {
532+ let mut t = change_list.save_children_to_temporaries(
533+ shared_prefix_count + start,
534+ shared_prefix_count + end,
535+ );
536+ for i in start..end {
537+ old_index_to_temp[i] = t;
538+ t += 1;
539+ }
540+ }
541+
542+ debug_assert!(end <= old.len());
543+ if end == old.len() {
544+ break;
229545 } else {
230− change_list.push_first_child();
546+ start = end + 1;
231547 }
232− change_list.remove_self_and_next_siblings();
233− pushed = false;
548+ }
549+
550+ // Remove any old children whose keys were not reused in the new
551+ // children. Remove from the end first so that we don't mess up indices.
552+ let mut removed_count = 0;
553+ for (i, old_child) in old.iter().enumerate().rev() {
554+ if !shared_keys.contains(&old_child.key()) {
555+ registry.remove_subtree(old_child);
556+ change_list.remove_child(i + shared_prefix_count);
557+ removed_count += 1;
558+ }
559+ }
560+
561+ // If there aren't any more new children, then we are done!
562+ if new.is_empty() {
563+ return;
564+ }
565+
566+ // The longest increasing subsequence within `new_index_to_old_index`. This
567+ // is the longest sequence on DOM nodes in `old` that are relatively ordered
568+ // correctly within `new`. We will leave these nodes in place in the DOM,
569+ // and only move nodes that are not part of the LIS. This results in the
570+ // maximum number of DOM nodes left in place, AKA the minimum number of DOM
571+ // nodes moved.
572+ let mut new_index_is_in_lis = FxHashSet::default();
573+ new_index_is_in_lis.reserve(new_index_to_old_index.len());
574+ let mut predecessors = vec![0; new_index_to_old_index.len()];
575+ let mut starts = vec![0; new_index_to_old_index.len()];
576+ longest_increasing_subsequence::lis_with(
577+ &new_index_to_old_index,
578+ &mut new_index_is_in_lis,
579+ |a, b| a < b,
580+ &mut predecessors,
581+ &mut starts,
582+ );
583+
584+ // Now we will iterate from the end of the new children back to the
585+ // beginning, diffing old children we are reusing and if they aren't in the
586+ // LIS moving them to their new destination, or creating new children. Note
587+ // that iterating in reverse order lets us use `Node.prototype.insertBefore`
588+ // to move/insert children.
589+ //
590+ // But first, we ensure that we have a child on the change list stack that
591+ // we can `insertBefore`. We handle this once before looping over `new`
592+ // children, so that we don't have to keep checking on every loop iteration.
593+ if shared_suffix_count > 0 {
594+ // There is a shared suffix after these middle children. We will be
595+ // inserting before that shared suffix, so add the first child of that
596+ // shared suffix to the change list stack.
597+ //
598+ // [... parent]
599+ change_list.push_child(old_shared_suffix_start - removed_count);
600+ // [... parent first_child_of_shared_suffix]
234601 } else {
235− debug!(" creating new children");
236− for (i, new_child) in new_children.enumerate() {
237− if i == 0 && pushed {
602+ // There is no shared suffix coming after these middle children.
603+ // Therefore we have to process the last child in `new` and move it to
604+ // the end of the parent's children if it isn't already there.
605+ let last_index = new.len() - 1;
606+ let last = new.last().unwrap_throw();
607+ new = &new[..new.len() - 1];
608+ if shared_keys.contains(&last.key()) {
609+ let old_index = new_index_to_old_index[last_index];
610+ let temp = old_index_to_temp[old_index];
611+ // [... parent]
612+ change_list.push_temporary(temp);
613+ // [... parent last]
614+ diff(
615+ cached_set,
616+ change_list,
617+ registry,
618+ &old[old_index],
619+ last,
620+ cached_roots,
621+ );
622+ if new_index_is_in_lis.contains(&last_index) {
623+ // Don't move it, since it is already where it needs to be.
624+ } else {
625+ change_list.append_child();
626+ // [... parent]
627+ change_list.push_temporary(temp);
628+ // [... parent last]
629+ }
630+ } else {
631+ // [... parent]
632+ create(cached_set, change_list, registry, last, cached_roots);
633+ // [... parent last]
634+ change_list.append_child();
635+ // [... parent]
636+ change_list.push_last_child();
637+ // [... parent last]
638+ }
639+ }
640+
641+ for (new_index, new_child) in new.iter().enumerate().rev() {
642+ let old_index = new_index_to_old_index[new_index];
643+ if old_index == u32::MAX as usize {
644+ debug_assert!(!shared_keys.contains(&new_child.key()));
645+ // [... parent successor]
646+ create(cached_set, change_list, registry, new_child, cached_roots);
647+ // [... parent successor new_child]
648+ change_list.insert_before();
649+ // [... parent new_child]
650+ } else {
651+ debug_assert!(shared_keys.contains(&new_child.key()));
652+ let temp = old_index_to_temp[old_index];
653+ debug_assert_ne!(temp, u32::MAX);
654+
655+ if new_index_is_in_lis.contains(&new_index) {
656+ // [... parent successor]
238657 change_list.pop();
239− pushed = false;
658+ // [... parent]
659+ change_list.push_temporary(temp);
660+ // [... parent new_child]
661+ } else {
662+ // [... parent successor]
663+ change_list.push_temporary(temp);
664+ // [... parent successor new_child]
665+ change_list.insert_before();
666+ // [... parent new_child]
240667 }
241− create(
668+
669+ diff(
242670 cached_set,
243671 change_list,
244672 registry,
245− new_child.clone(),
673+ &old[old_index],
674+ new_child,
246675 cached_roots,
247676 );
248− change_list.append_child();
249677 }
250678 }
251679
252− debug!(" done updating children");
253− if pushed {
254− change_list.pop();
680+ // [... parent child]
681+ change_list.pop();
682+ // [... parent]
683+}
684+
685+// Diff the suffix of keyed children that share the same keys in the same order.
686+//
687+// The parent must be on the change list stack when we enter this function:
688+//
689+// [... parent]
690+//
691+// When this function exits, the change list stack remains the same.
692+fn diff_keyed_suffix(
693+ cached_set: &CachedSet,
694+ change_list: &mut ChangeListBuilder,
695+ registry: &mut EventsRegistry,
696+ old: &[Node],
697+ new: &[Node],
698+ cached_roots: &mut FxHashSet<CacheId>,
699+ new_shared_suffix_start: usize,
700+) {
701+ debug_assert_eq!(old.len(), new.len());
702+ debug_assert!(!old.is_empty());
703+
704+ // [... parent]
705+ change_list.push_child(new_shared_suffix_start);
706+ // [... parent new_child]
707+
708+ for (old_child, new_child) in old.iter().zip(new.iter()) {
709+ diff(
710+ cached_set,
711+ change_list,
712+ registry,
713+ old_child,
714+ new_child,
715+ cached_roots,
716+ );
717+
718+ // [... parent this_new_child]
719+ change_list.pop_push_next_sibling();
720+ // [... parent next_new_child]
721+ }
722+
723+ // [... parent]
724+ change_list.pop();
725+}
726+
727+// Diff children that are not keyed.
728+//
729+// The parent must be on the top of the change list stack when entering this
730+// function:
731+//
732+// [... parent]
733+//
734+// the change list stack is in the same state when this function returns.
735+fn diff_non_keyed_children(
736+ cached_set: &CachedSet,
737+ change_list: &mut ChangeListBuilder,
738+ registry: &mut EventsRegistry,
739+ old: &[Node],
740+ new: &[Node],
741+ cached_roots: &mut FxHashSet<CacheId>,
742+) {
743+ // Handled these cases in `diff_children` before calling this function.
744+ debug_assert!(!new.is_empty());
745+ debug_assert!(!old.is_empty());
746+
747+ for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() {
748+ if i == 0 {
749+ // [... parent]
750+ change_list.push_first_child();
751+ // [... parent first_child]
752+ } else {
753+ // [... parent prev_sibling]
754+ change_list.pop_push_next_sibling();
755+ // [... parent next_sibling]
756+ }
757+
758+ diff(
759+ cached_set,
760+ change_list,
761+ registry,
762+ old_child,
763+ new_child,
764+ cached_roots,
765+ );
766+ }
767+
768+ // Note that because `new` and `old` are not empty, the previous loop always
769+ // executes at least once, so the change list stack is now:
770+ //
771+ // [... parent child]
772+
773+ match old.len().cmp(&new.len()) {
774+ Ordering::Greater => {
775+ // [... parent last_shared_child]
776+ change_list.pop_push_next_sibling();
777+ // [... parent first_child_to_remove]
778+ remove_self_and_next_siblings(change_list, registry, &old[new.len()..]);
779+ // [... parent]
780+ }
781+ Ordering::Less => {
782+ // [... parent last_child]
783+ change_list.pop();
784+ // [... parent]
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+ // [... parent child]
795+ change_list.pop();
796+ // [... parent]
797+ }
255798 }
256799}
257800
801+// Create the given children and append them to the parent node.
802+//
803+// The parent node must currently be on top of the change list stack:
804+//
805+// [... parent]
806+//
807+// When this function returns, the change list stack is in the same state.
808+fn create_and_append_children(
809+ cached_set: &CachedSet,
810+ change_list: &mut ChangeListBuilder,
811+ registry: &mut EventsRegistry,
812+ new: &[Node],
813+ cached_roots: &mut FxHashSet<CacheId>,
814+) {
815+ for child in new {
816+ create(cached_set, change_list, registry, child, cached_roots);
817+ change_list.append_child();
818+ }
819+}
820+
821+// Remove all of a node's children.
822+//
823+// The change list stack must have this shape upon entry to this function:
824+//
825+// [... parent]
826+//
827+// When this function returns, the change list stack is in the same state.
828+fn remove_all_children(
829+ change_list: &mut ChangeListBuilder,
830+ registry: &mut EventsRegistry,
831+ old: &[Node],
832+) {
833+ for child in old {
834+ registry.remove_subtree(child);
835+ }
836+ // Fast way to remove all children: set the node's textContent to an empty
837+ // string.
838+ change_list.set_text("");
839+}
840+
841+// Remove the current child and all of its following siblings.
842+//
843+// The change list stack must have this shape upon entry to this function:
844+//
845+// [... parent child]
846+//
847+// After the function returns, the child is no longer on the change list stack:
848+//
849+// [... parent]
850+fn remove_self_and_next_siblings(
851+ change_list: &mut ChangeListBuilder,
852+ registry: &mut EventsRegistry,
853+ old: &[Node],
854+) {
855+ for child in old {
856+ registry.remove_subtree(child);
857+ }
858+ change_list.remove_self_and_next_siblings();
859+}
860+
861+// Emit instructions to create the given virtual node.
862+//
863+// The change list stack may have any shape upon entering this function:
864+//
865+// [...]
866+//
867+// When this function returns, the new node is on top of the change list stack:
868+//
869+// [... node]
258870fn create(
259871 cached_set: &CachedSet,
260872 change_list: &mut ChangeListBuilder,
261873 registry: &mut EventsRegistry,
262− node: Node,
874+ node: &Node,
263875 cached_roots: &mut FxHashSet<CacheId>,
264876) {
265877 match node.kind {
266878 NodeKind::Text(TextNode { text }) => {
267879 change_list.create_text_node(text);
268880 }
269− NodeKind::Element(ElementNode {
881+ NodeKind::Element(&ElementNode {
882+ key: _,
270883 tag_name,
271884 listeners,
272885 attributes,
@@ -288,17 +901,11 @@ fn create(
288901 change_list.set_attribute(&attr.name, &attr.value);
289902 }
290903 for child in children {
291− create(
292− cached_set,
293− change_list,
294− registry,
295− child.clone(),
296− cached_roots,
297− );
904+ create(cached_set, change_list, registry, child, cached_roots);
298905 change_list.append_child();
299906 }
300907 }
301− NodeKind::Cached(c) => {
908+ NodeKind::Cached(ref c) => {
302909 cached_roots.insert(c.id);
303910 let node = cached_set.get(c.id);
304911 create(cached_set, change_list, registry, node, cached_roots)
src/events.rs+8−17
@@ -19,7 +19,6 @@ cfg_if::cfg_if! {
1919 pub(crate) fn clear_active_listeners(&mut self) {}
2020 }
2121 } else {
22− use bumpalo::Bump;
2322 use crate::{
2423 node::{ElementNode, ListenerCallback, NodeKind},
2524 vdom::VdomWeak,
@@ -64,7 +63,6 @@ cfg_if::cfg_if! {
6463
6564 let weak_registry = Rc::downgrade(&registry);
6665 let closure = Closure::wrap(Box::new(move |event, a, b| {
67− debug!("Events trampoline invoked with (0x{:x}, 0x{:x})", a, b);
6866 debug_assert!(a != 0);
6967
7068 // if the VdomInnerExclusive is keeping this closure alive, then the
@@ -95,24 +93,19 @@ cfg_if::cfg_if! {
9593
9694 pub(crate) fn remove(&mut self, listener: &Listener) {
9795 let id = listener.get_callback_parts();
98− debug!("EventsRegistry::remove(0x{:x}, 0x{:x})", id.0, id.1);
9996 debug_assert!(id.0 != 0);
10097 self.active.remove(&id);
10198 }
10299
103100 pub(crate) fn remove_subtree(&mut self, node: &Node) {
104− let bump = Bump::new();
105− let mut stack = bumpalo::collections::Vec::with_capacity_in(64, &bump);
106− stack.push(node);
107−
108− while let Some(node) = stack.pop() {
109− match node.kind {
110− NodeKind::Cached(_) | NodeKind::Text(_) => continue,
111− NodeKind::Element(ElementNode {listeners, children, ..}) => {
112− for l in listeners {
113− self.remove(l);
114− }
115− stack.extend(children);
101+ match node.kind {
102+ NodeKind::Cached(_) | NodeKind::Text(_) => return,
103+ NodeKind::Element(&ElementNode {listeners, children, ..}) => {
104+ for l in listeners {
105+ self.remove(l);
106+ }
107+ for child in children {
108+ self.remove_subtree(child)
116109 }
117110 }
118111 }
@@ -128,7 +121,6 @@ cfg_if::cfg_if! {
128121 /// diffing.
129122 pub(crate) unsafe fn add<'a>(&mut self, listener: &'a Listener<'a>) {
130123 let id = listener.get_callback_parts();
131− debug!("EventsRegistry::add(0x{:x}, 0x{:x})", id.0, id.1);
132124 debug_assert!(id.0 != 0);
133125
134126 let callback =
@@ -139,7 +131,6 @@ cfg_if::cfg_if! {
139131
140132 /// Clear all event listeners from the registry.
141133 pub(crate) fn clear_active_listeners(&mut self) {
142− debug!("EventsRegistry::clear_active_listeners()");
143134 self.active.clear();
144135 }
145136 }
src/node.rs+53−20
@@ -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)]
@@ -24,11 +24,11 @@ pub_unstable_internal! {
2424 Text(TextNode<'a>),
2525
2626 /// An element potentially with attributes and children.
27− Element(ElementNode<'a>),
27+ Element(&'a ElementNode<'a>),
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
@@ -46,6 +46,7 @@ pub_unstable_internal! {
4646 /// children.
4747 #[derive(Debug, Clone)]
4848 pub(crate) struct ElementNode<'a> {
49+ pub key: NodeKey,
4950 pub tag_name: &'a str,
5051 pub listeners: &'a [Listener<'a>],
5152 pub attributes: &'a [Attribute<'a>],
@@ -59,10 +60,31 @@ pub_unstable_internal! {
5960 /// allows us to avoid both re-rendering a sub-tree and re-diffing
6061 /// it. `CachedNode`s cannot contain cycles, since they can only contain
6162 /// other `CachedNode`s that already exist when they are being constructed.
62− #[derive(Debug, Clone)]
63− pub(crate) struct CachedNode<'a> {
63+ #[derive(Debug, Clone, Copy)]
64+ pub(crate) struct CachedNode {
6465 pub id: CacheId,
65− pub _phantom: PhantomData<&'a Node<'a>>,
66+ // Always a copy of the inner cached node's key.
67+ pub key: NodeKey,
68+ }
69+}
70+
71+pub_unstable_internal! {
72+ /// The key for keyed children.
73+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
74+ pub(crate) struct NodeKey(pub(crate) u32);
75+}
76+
77+impl NodeKey {
78+ pub const NONE: NodeKey = NodeKey(u32::MAX);
79+
80+ #[inline]
81+ pub fn is_none(&self) -> bool {
82+ *self == Self::NONE
83+ }
84+
85+ #[inline]
86+ pub fn is_some(&self) -> bool {
87+ !self.is_none()
6688 }
6789}
6890
@@ -92,6 +114,15 @@ pub struct Attribute<'a> {
92114 pub(crate) value: &'a str,
93115}
94116
117+impl<'a> From<CachedNode> for Node<'a> {
118+ #[inline]
119+ fn from(c: CachedNode) -> Self {
120+ Node {
121+ kind: NodeKind::Cached(c),
122+ }
123+ }
124+}
125+
95126impl fmt::Debug for Listener<'_> {
96127 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
97128 let (a, b) = self.get_callback_parts();
@@ -135,6 +166,7 @@ impl<'a> Node<'a> {
135166 #[inline]
136167 pub(crate) fn element<Listeners, Attributes, Children>(
137168 bump: &'a Bump,
169+ key: NodeKey,
138170 tag_name: &'a str,
139171 listeners: Listeners,
140172 attributes: Attributes,
@@ -155,14 +187,17 @@ impl<'a> Node<'a> {
155187 let attributes: &'a Attributes = bump.alloc(attributes);
156188 let attributes: &'a [Attribute<'a>] = attributes.as_ref();
157189
190+ let element = bump.alloc_with(|| ElementNode {
191+ key,
192+ tag_name,
193+ listeners,
194+ attributes,
195+ children,
196+ namespace,
197+ });
198+
158199 Node {
159− kind: NodeKind::Element(ElementNode {
160− tag_name,
161− listeners,
162− attributes,
163− children,
164− namespace,
165− }),
200+ kind: NodeKind::Element(element),
166201 }
167202 }
168203
@@ -174,14 +209,12 @@ impl<'a> Node<'a> {
174209 }
175210 }
176211
177− /// Construct a new cached node with the given id.
178212 #[inline]
179− pub(crate) fn cached(id: CacheId) -> Node<'a> {
180− Node {
181− kind: NodeKind::Cached(CachedNode {
182− id,
183− _phantom: PhantomData,
184− }),
213+ pub(crate) fn key(&self) -> NodeKey {
214+ match &self.kind {
215+ NodeKind::Text(_) => NodeKey::NONE,
216+ NodeKind::Element(e) => e.key,
217+ NodeKind::Cached(c) => c.key,
185218 }
186219 }
187220}
src/vdom.rs+4−6
@@ -2,7 +2,7 @@ use super::change_list::ChangeListPersistentState;
22use super::RootRender;
33use crate::cached_set::CachedSet;
44use crate::events::EventsRegistry;
5−use crate::node::Node;
5+use crate::node::{Node, NodeKey};
66use crate::RenderContext;
77use bumpalo::Bump;
88use futures::future::Future;
@@ -166,7 +166,7 @@ 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 = Node::element(&dom_buffers[0], NodeKey::NONE, "div", [], [], [], None);
170170 let current_root = Some(unsafe { extend_node_lifetime(current_root) });
171171
172172 let container = container.clone();
@@ -277,8 +277,8 @@ impl VdomInnerExclusive {
277277 &cached_set,
278278 &mut change_list,
279279 &mut registry,
280− old_contents,
281− new_contents.clone(),
280+ &old_contents,
281+ &new_contents,
282282 &mut cache_roots,
283283 );
284284
@@ -424,7 +424,6 @@ impl VdomWeak {
424424 /// If you don't want to do more things after the render completes, then use
425425 /// `schedule_render` instead of `render`.
426426 pub fn render(&self) -> impl Future<Item = (), Error = VdomDroppedError> {
427− debug!("VdomWeak::render: initiating render in new animation frame");
428427 futures::future::ok(self.inner.upgrade())
429428 .and_then(|inner| inner.ok_or(()))
430429 .map_err(|_| VdomDroppedError {})
@@ -449,7 +448,6 @@ impl VdomWeak {
449448 // animation frames.
450449 let _ = inner.shared.render_scheduled.take();
451450
452− debug!("VdomWeak::render: finished rendering");
453451 let r = resolve.call0(&JsValue::null());
454452 debug_assert!(r.is_ok());
455453 }
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+270
@@ -0,0 +1,270 @@
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");
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+ .children([
224+ li(&cx)
225+ .key(1)
226+ .children([
227+ keyed(cx, [2, 3, 4])
228+ ])
229+ .finish(),
230+ li(&cx)
231+ .key(5)
232+ .children([
233+ keyed(cx, [6, 7, 8])
234+ ])
235+ .finish(),
236+ li(&cx)
237+ .key(9)
238+ .children([
239+ keyed(cx, [10, 11, 12])
240+ ])
241+ .finish(),
242+ ])
243+ .finish()
244+ }
245+ after(cx) {
246+ ul(&cx)
247+ .children([
248+ li(&cx)
249+ .key(9)
250+ .children([
251+ keyed(cx, [12, 11, 10])
252+ ])
253+ .finish(),
254+ li(&cx)
255+ .key(5)
256+ .children([
257+ keyed(cx, [8, 7, 6])
258+ ])
259+ .finish(),
260+ li(&cx)
261+ .key(1)
262+ .children([
263+ keyed(cx, [4, 3, 2])
264+ ])
265+ .finish(),
266+ ])
267+ .finish()
268+ }
269+ }
270+}
tests/web/main.rs+2−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 {
@@ -88,7 +89,7 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
8889 "actual.text_content() == expected.text()"
8990 );
9091 }
91− NodeKind::Element(ElementNode {
92+ NodeKind::Element(&ElementNode {
9293 tag_name,
9394 attributes,
9495 children,