Merge pull request #2433 from sveltejs/gh-2430

Use Map for keyed each block lookups
pull/2436/head
Rich Harris 5 years ago committed by GitHub
commit 5d71b5138a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -198,7 +198,7 @@ An each block can also specify an *index*, equivalent to the second argument in
---
If a *key* expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end.
If a *key* expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
```html
{#each items as item, i (item.id)}

@ -14,4 +14,6 @@ To do that, we specify a unique identifier for the `each` block:
{/each}
```
The `(thing.id)` tells Svelte how to figure out what changed.
The `(thing.id)` tells Svelte how to figure out what changed.
> You can use any object as the key, as Svelte uses a `Map` internally — in other words you could do `(thing)` instead of `(thing.id)`. Using a string or number is generally safer, however, since it means identity persists without referential equality, for example when updating with fresh data from an API server.

@ -294,7 +294,7 @@ export default class EachBlockWrapper extends Wrapper {
const lookup = block.get_unique_name(`${this.var}_lookup`);
block.add_variable(iterations, '[]');
block.add_variable(lookup, `@blank_object()`);
block.add_variable(lookup, `new Map()`);
if (this.fragment.nodes[0].is_dom_node()) {
this.block.first = this.fragment.nodes[0].var;
@ -314,7 +314,7 @@ export default class EachBlockWrapper extends Wrapper {
for (var #i = 0; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
let child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i);
let key = ${get_key}(child_ctx);
${iterations}[#i] = ${lookup}[key] = ${create_each_block}(key, child_ctx);
${lookup}.set(key, ${iterations}[#i] = ${create_each_block}(key, child_ctx));
}
`);

@ -2,7 +2,7 @@ import { on_outro } from './transitions.js';
export function destroy_block(block, lookup) {
block.d(1);
lookup[block.key] = null;
lookup.delete(block.key);
}
export function outro_and_destroy_block(block, lookup) {
@ -27,14 +27,14 @@ export function update_keyed_each(old_blocks, changed, get_key, dynamic, ctx, li
while (i--) old_indexes[old_blocks[i].key] = i;
const new_blocks = [];
const new_lookup = {};
const deltas = {};
const new_lookup = new Map();
const deltas = new Map();
i = n;
while (i--) {
const child_ctx = get_context(ctx, list, i);
const key = get_key(child_ctx);
let block = lookup[key];
let block = lookup.get(key);
if (!block) {
block = create_each_block(key, child_ctx);
@ -43,18 +43,18 @@ export function update_keyed_each(old_blocks, changed, get_key, dynamic, ctx, li
block.p(changed, child_ctx);
}
new_blocks[i] = new_lookup[key] = block;
new_lookup.set(key, new_blocks[i] = block);
if (key in old_indexes) deltas[key] = Math.abs(i - old_indexes[key]);
if (key in old_indexes) deltas.set(key, Math.abs(i - old_indexes[key]));
}
const will_move = {};
const did_move = {};
const will_move = new Set();
const did_move = new Set();
function insert(block) {
if (block.i) block.i(1);
block.m(node, next);
lookup[block.key] = block;
lookup.set(block.key, block);
next = block.first;
n--;
}
@ -72,32 +72,32 @@ export function update_keyed_each(old_blocks, changed, get_key, dynamic, ctx, li
n--;
}
else if (!new_lookup[old_key]) {
else if (!new_lookup.has(old_key)) {
// remove old block
destroy(old_block, lookup);
o--;
}
else if (!lookup[new_key] || will_move[new_key]) {
else if (!lookup.has(new_key) || will_move.has(new_key)) {
insert(new_block);
}
else if (did_move[old_key]) {
else if (did_move.has(old_key)) {
o--;
} else if (deltas[new_key] > deltas[old_key]) {
did_move[new_key] = true;
} else if (deltas.get(new_key) > deltas.get(old_key)) {
did_move.add(new_key);
insert(new_block);
} else {
will_move[old_key] = true;
will_move.add(old_key);
o--;
}
}
while (o--) {
const old_block = old_blocks[o];
if (!new_lookup[old_block.key]) destroy(old_block, lookup);
if (!new_lookup.has(old_block.key)) destroy(old_block, lookup);
}
while (n) insert(new_blocks[n - 1]);

@ -2,7 +2,6 @@
import {
SvelteComponent,
append,
blank_object,
create_animation,
detach,
element,
@ -73,7 +72,7 @@ function create_each_block(key_1, ctx) {
}
function create_fragment(ctx) {
var each_blocks = [], each_1_lookup = blank_object(), each_1_anchor;
var each_blocks = [], each_1_lookup = new Map(), each_1_anchor;
var each_value = ctx.things;
@ -82,7 +81,7 @@ function create_fragment(ctx) {
for (var i = 0; i < each_value.length; i += 1) {
let child_ctx = get_each_context(ctx, each_value, i);
let key = get_key(child_ctx);
each_blocks[i] = each_1_lookup[key] = create_each_block(key, child_ctx);
each_1_lookup.set(key, each_blocks[i] = create_each_block(key, child_ctx));
}
return {

@ -2,7 +2,6 @@
import {
SvelteComponent,
append,
blank_object,
destroy_block,
detach,
element,
@ -57,7 +56,7 @@ function create_each_block(key_1, ctx) {
}
function create_fragment(ctx) {
var each_blocks = [], each_1_lookup = blank_object(), each_1_anchor;
var each_blocks = [], each_1_lookup = new Map(), each_1_anchor;
var each_value = ctx.things;
@ -66,7 +65,7 @@ function create_fragment(ctx) {
for (var i = 0; i < each_value.length; i += 1) {
let child_ctx = get_each_context(ctx, each_value, i);
let key = get_key(child_ctx);
each_blocks[i] = each_1_lookup[key] = create_each_block(key, child_ctx);
each_1_lookup.set(key, each_blocks[i] = create_each_block(key, child_ctx));
}
return {

@ -0,0 +1,25 @@
export default {
props: {
todos: [
{ description: 'implement keyed each blocks' },
{ description: 'implement client-side hydration' }
]
},
html: `
<p>1: implement keyed each blocks</p>
<p>2: implement client-side hydration</p>
`,
test({ assert, component, target }) {
const [p1, p2] = target.querySelectorAll('p');
component.todos = [component.todos[1]];
assert.htmlEqual(target.innerHTML, '<p>1: implement client-side hydration</p>');
const [p3] = target.querySelectorAll('p');
assert.ok(!target.contains(p1), 'first <p> element should be removed');
assert.equal(p2, p3, 'second <p> element should be retained');
}
};

@ -0,0 +1,7 @@
<script>
export let todos;
</script>
{#each todos as todo, i (todo)}
<p>{i+1}: {todo.description}</p>
{/each}
Loading…
Cancel
Save