Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A new list diffing algorithm #1274

Merged
merged 13 commits into from
Mar 24, 2018
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ src/generators/dom/shared.ts
package-lock.json
.idea/
*.iml
store.umd.js
store.umd.js
yarn-error.log
53 changes: 9 additions & 44 deletions src/generators/nodes/EachBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,18 +246,11 @@ export default class EachBlock extends Node {
}
) {
const key = block.getUniqueName('key');
const blocks = block.getUniqueName(`${each}_blocks`);
const lookup = block.getUniqueName(`${each}_lookup`);
const iteration = block.getUniqueName(`${each}_iteration`);
const head = block.getUniqueName(`${each}_head`);
const last = block.getUniqueName(`${each}_last`);
const expected = block.getUniqueName(`${each}_expected`);
const keep = block.getUniqueName(`${each}_keep`);
const mounts = block.getUniqueName(`${each}_mounts`);
const next_iteration = block.getUniqueName(`${each}_next_iteration`);

block.addVariable(blocks, '[]');
block.addVariable(lookup, `@blankObject()`);
block.addVariable(head);
block.addVariable(last);

if (this.children[0].isDomNode()) {
this.block.first = this.children[0].var;
Expand All @@ -274,15 +267,9 @@ export default class EachBlock extends Node {
block.builders.init.addBlock(deindent`
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
var ${key} = ${each_block_value}[#i].${this.key};
var ${iteration} = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign({}, state, {
${blocks}[#i] = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign({}, state, {
${this.contextProps.join(',\n')}
}));

if (${last}) ${last}.next = ${iteration};
${iteration}.last = ${last};
${last} = ${iteration};

if (#i === 0) ${head} = ${iteration};
}
`);

Expand All @@ -291,61 +278,39 @@ export default class EachBlock extends Node {
const anchorNode = parentNode ? 'null' : 'anchor';

block.builders.create.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.c();
${iteration} = ${iteration}.next;
}
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].c();
`);

if (parentNodes) {
block.builders.claim.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.l(${parentNodes});
${iteration} = ${iteration}.next;
}
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].l(${parentNodes});
`);
}

block.builders.mount.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.${mountOrIntro}(${initialMountNode}, ${anchorNode});
${iteration} = ${iteration}.next;
}
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode});
`);

const dynamic = this.block.hasUpdateMethod;

block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet};

@updateKeyedEach(#component, ${key}, changed, "${this.key}", ${dynamic}, ${each_block_value}, ${head}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", function(#i) {
${blocks} = @updateKeyedEach(${blocks}, #component, changed, "${this.key}", ${dynamic}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", function(#i) {
return @assign({}, state, {
${this.contextProps.join(',\n')}
});
});

${head} = ${lookup}[${each_block_value}[0] && ${each_block_value}[0].${this.key}];
`);

if (!parentNode) {
block.builders.unmount.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.u();
${iteration} = ${iteration}.next;
}
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].u();
`);
}

block.builders.destroy.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.d();
${iteration} = ${iteration}.next;
}
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].d();
`);
}

Expand Down
129 changes: 74 additions & 55 deletions src/shared/keyed-each.js
Original file line number Diff line number Diff line change
@@ -1,80 +1,99 @@
import { assign } from './utils.js';

export function destroyIteration(iteration, lookup) {
var first = iteration.first
if (first && first.parentNode) {
iteration.u();
}
iteration.d();
lookup[iteration.key] = null;
export function destroyBlock(block, lookup) {
block.u();
block.d();
lookup[block.key] = null;
}

export function outroAndDestroyIteration(iteration, lookup) {
iteration.o(function() {
iteration.u();
iteration.d();
lookup[iteration.key] = null;
export function outroAndDestroyBlock(block, lookup) {
block.o(function() {
destroyBlock(block, lookup);
});
}

// TODO is it possible to avoid mounting iterations that are
// already in the right place?
export function updateKeyedEach(component, key, changed, key_prop, dynamic, list, head, lookup, node, has_outro, create_each_block, intro_method, get_context) {
var keep = {};
export function updateKeyedEach(old_blocks, component, changed, key_prop, dynamic, list, lookup, node, has_outro, create_each_block, intro_method, get_context) {
var o = old_blocks.length;
var n = list.length;

var i = o;
var old_indexes = {};
while (i--) old_indexes[old_blocks[i].key] = i;

var i = list.length;
var new_blocks = [];
var new_lookup = {};
var deltas = {};

var i = n;
while (i--) {
var key = list[i][key_prop];
var iteration = lookup[key];
var block = lookup[key];

if (iteration) {
if (dynamic) iteration.p(changed, get_context(i));
} else {
iteration = lookup[key] = create_each_block(component, key, get_context(i));
iteration.c();
if (!block) {
block = create_each_block(component, key, get_context(i));
block.c();
} else if (dynamic) {
block.p(changed, get_context(i));
}

lookup[key] = iteration;
keep[key] = 1;
new_blocks[i] = new_lookup[key] = block;

if (key in old_indexes) deltas[key] = Math.abs(i - old_indexes[key]);
}

var destroy = has_outro
? outroAndDestroyIteration
: destroyIteration;
var next = null;

var will_move = {};
var did_move = {};

iteration = head;
while (iteration) {
if (!keep[iteration.key]) destroy(iteration, lookup);
iteration = iteration.next;
var destroy = has_outro ? outroAndDestroyBlock : destroyBlock;

function insert(block) {
block[intro_method](node, next && next.first);
next = lookup[block.key] = block;
n--;
}

var next = null;
while (o && n) {
var new_block = new_blocks[n - 1];
var old_block = old_blocks[o - 1];
var new_key = new_block.key;
var old_key = old_block.key;

i = list.length;
while (i--) {
key = list[i][key_prop];
iteration = lookup[key];
if (new_block === old_block) {
// do nothing
next = new_block;
o--;
n--;
}

else if (!new_lookup[old_key]) {
// remove old block
destroy(old_block, lookup);
o--;
}

else if (!lookup[new_key] || will_move[new_key]) {
insert(new_block);
}

var anchor;
else if (did_move[old_key]) {
o--;

if (has_outro) {
var next_key = next && next.key;
var neighbour = iteration.next;
var anchor_key;
} else if (deltas[new_key] > deltas[old_key]) {
did_move[new_key] = true;
insert(new_block);

while (neighbour && anchor_key != next_key && !keep[anchor_key]) {
anchor = neighbour && neighbour.first;
neighbour = neighbour.next;
anchor_key = neighbour && neighbour.key;
}
} else {
anchor = next && next.first;
will_move[old_key] = true;
o--;
}
}

iteration[intro_method](node, anchor);

iteration.next = next;
if (next) next.last = iteration;
next = iteration;
while (o--) {
var old_block = old_blocks[o];
if (!new_lookup[old_block.key]) destroy(old_block, lookup);
}

while (n) insert(new_blocks[n - 1]);

return new_blocks;
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export default {
test('hdnkjougmrvftewsqpailcb');
test('bidhfacge');
test('kgjnempcboaflidh');
test('fekbijachgd');
test('kdmlgfbicheja');

// then, we party
for (let i = 0; i < 100; i += 1) test(permute());
Expand Down