From 2d10d535e567b2df8fe69a6bd3f14da81a259f13 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 18 Mar 2018 14:34:19 -0400 Subject: [PATCH 1/3] more keyed each diffing into a shared helper --- src/generators/nodes/EachBlock.ts | 98 +------------------------------ src/shared/_build.js | 2 +- src/shared/index.js | 1 + src/shared/keyed-each.js | 92 +++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 96 deletions(-) create mode 100644 src/shared/keyed-each.js diff --git a/src/generators/nodes/EachBlock.ts b/src/generators/nodes/EachBlock.ts index 6aa9ab673e..ffc95eb485 100644 --- a/src/generators/nodes/EachBlock.ts +++ b/src/generators/nodes/EachBlock.ts @@ -315,108 +315,16 @@ export default class EachBlock extends Node { `); const dynamic = this.block.hasUpdateMethod; - let fn_destroy; - if (this.block.hasOutroMethod) { - fn_destroy = block.getUniqueName(`${each}_outro`); - block.builders.init.addBlock(deindent` - function ${fn_destroy}(iteration) { - iteration.o(function() { - iteration.u(); - iteration.d(); - ${lookup}[iteration.key] = null; - }); - } - `); - } else { - fn_destroy = block.getUniqueName(`${each}_destroy`); - block.builders.init.addBlock(deindent` - function ${fn_destroy}(iteration) { - var first = iteration.first - if (first && first.parentNode) { - iteration.u(); - } - iteration.d(); - ${lookup}[iteration.key] = null; - } - `); - } - const destroy = deindent` - ${iteration} = ${head}; - while(${iteration}) { - if (!${keep}[${iteration}.key]) { - ${fn_destroy}(${iteration}); - } - ${iteration} = ${iteration}.next; - } - `; block.builders.update.addBlock(deindent` var ${each_block_value} = ${snippet}; - var ${expected} = ${head}; - var ${last} = null; - - var ${keep} = {}; - var ${mounts} = {}; - var ${next_iteration} = null; - for (#i = 0; #i < ${each_block_value}.${length}; #i += 1) { - var ${key} = ${each_block_value}[#i].${this.key}; - var ${iteration} = ${lookup}[${key}]; - var next_data = ${each_block_value}[#i+1]; - var next = next_data && ${lookup}[next_data.${this.key}]; - - var ${this.each_context} = @assign({}, state, { + @updateKeyedEach(#component, ${key}, changed, "${this.key}", ${dynamic}, ${each_block_value}, ${head}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, function(#i) { + return @assign({}, state, { ${this.contextProps.join(',\n')} }); + }); - ${dynamic && - `if (${iteration}) ${iteration}.p(changed, ${this.each_context});`} - if (${expected} && (${key} === ${expected}.key)) { - var first = ${iteration} && ${iteration}.first; - var parentNode = first && first.parentNode - if (!parentNode || (${iteration} && ${iteration}.next) != next) ${mounts}[${key}] = ${iteration}; - ${expected} = ${iteration}.next; - } else if (${iteration}) { - ${mounts}[${key}] = ${iteration}; - ${expected} = ${iteration}.next; - } else { - // key is being inserted - ${iteration} = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, ${this.each_context}); - ${iteration}.c(); - ${mounts}[${key}] = ${iteration}; - } - ${lookup}[${key}] = ${iteration}; - ${keep}[${iteration}.key] = ${iteration}; - ${last} = ${iteration}; - } - ${destroy} - - // Work backwards due to DOM api having insertBefore - for (#i = ${each_block_value}.${length} - 1; #i >= 0; #i -= 1) { - var data = ${each_block_value}[#i]; - var ${key} = data.${this.key}; - ${iteration} = ${lookup}[${key}]; - if (${mounts}[${key}]) { - var anchor; - ${this.block.hasOutroMethod - ? deindent` - var key_next_iteration = ${next_iteration} && ${next_iteration}.key; - var iteration_anchor = ${iteration}.next; - var key_anchor; - do { - anchor = iteration_anchor && iteration_anchor.first; - iteration_anchor = iteration_anchor && iteration_anchor.next; - key_anchor = iteration_anchor && iteration_anchor.key; - } while(iteration_anchor && key_anchor != key_next_iteration && !${keep}[key_anchor])` - : deindent` - anchor = ${next_iteration} && ${next_iteration}.first; - ` } - ${mounts}[${key}].${mountOrIntro}(${updateMountNode}, anchor); - } - ${iteration}.next = ${next_iteration}; - if (${next_iteration}) ${next_iteration}.last = ${iteration}; - ${next_iteration} = ${iteration}; - } ${head} = ${lookup}[${each_block_value}[0] && ${each_block_value}[0].${this.key}]; `); diff --git a/src/shared/_build.js b/src/shared/_build.js index 0339ba558c..3cc523e7cd 100644 --- a/src/shared/_build.js +++ b/src/shared/_build.js @@ -5,7 +5,7 @@ const acorn = require('acorn'); const declarations = {}; fs.readdirSync(__dirname).forEach(file => { - if (!/^[a-z]+\.js$/.test(file)) return; + if (!/^[a-z\-]+\.js$/.test(file)) return; const source = fs.readFileSync(path.join(__dirname, file), 'utf-8'); const ast = acorn.parse(source, { diff --git a/src/shared/index.js b/src/shared/index.js index ab8a053151..a410e7d9df 100644 --- a/src/shared/index.js +++ b/src/shared/index.js @@ -1,6 +1,7 @@ import { assign } from './utils.js'; import { noop } from './utils.js'; export * from './dom.js'; +export * from './keyed-each.js'; export * from './transitions.js'; export * from './utils.js'; diff --git a/src/shared/keyed-each.js b/src/shared/keyed-each.js new file mode 100644 index 0000000000..fd9ad18c59 --- /dev/null +++ b/src/shared/keyed-each.js @@ -0,0 +1,92 @@ +import { assign } from './utils.js'; + +export function updateKeyedEach(component, key, changed, block_key, dynamic, each_block_value, head, lookup, updateMountNode, hasOutroMethod, create_each_block, get_context) { + var last = null; + var expected = head; + + var keep = {}; + var mounts = {}; + var next_iteration = null; + + for (i = 0; i < each_block_value.length; i += 1) { + var key = each_block_value[i][block_key]; + var iteration = lookup[key]; + var next_data = each_block_value[i+1]; + var next = next_data && lookup[next_data[block_key]]; + + if (dynamic && iteration) iteration.p(changed, get_context(i)); // TODO should this be deferred? could it be redundant? + + if (expected && (key === expected.key)) { + var first = iteration && iteration.first; + var parentNode = first && first.parentNode + if (!parentNode || (iteration && iteration.next) != next) mounts[key] = iteration; + expected = iteration.next; + } else if (iteration) { + mounts[key] = iteration; + expected = iteration.next; + } else { + // key is being inserted + iteration = lookup[key] = create_each_block(component, key, get_context(i)); + iteration.c(); + mounts[key] = iteration; + } + lookup[key] = iteration; + keep[iteration.key] = iteration; + last = iteration; + } + + var destroy = hasOutroMethod + ? function(iteration) { + iteration.o(function() { + iteration.u(); + iteration.d(); + lookup[iteration.key] = null; + }); + } + : function(iteration) { + var first = iteration.first + if (first && first.parentNode) { + iteration.u(); + } + iteration.d(); + lookup[iteration.key] = null; + } + + iteration = head; + while(iteration) { + if (!keep[iteration.key]) { + destroy(iteration); + } + iteration = iteration.next; + } + + // Work backwards due to DOM api having insertBefore + for (i = each_block_value.length - 1; i >= 0; i -= 1) { + var data = each_block_value[i]; + var key = data[block_key]; + iteration = lookup[key]; + + var block = mounts[key]; + if (block) { + var anchor; + + if (hasOutroMethod) { + var key_next_iteration = next_iteration && next_iteration.key; + var iteration_anchor = iteration.next; + var key_anchor; + do { + anchor = iteration_anchor && iteration_anchor.first; + iteration_anchor = iteration_anchor && iteration_anchor.next; + key_anchor = iteration_anchor && iteration_anchor.key; + } while(iteration_anchor && key_anchor != key_next_iteration && !keep[key_anchor]) + } else { + anchor = next_iteration && next_iteration.first; + } + + block[block.i ? 'i' : 'm'](updateMountNode, anchor); + } + iteration.next = next_iteration; + if (next_iteration) next_iteration.last = iteration; + next_iteration = iteration; + } +} \ No newline at end of file From ad0ce6fde23d8beb74a068d18a7278f7b1564238 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 18 Mar 2018 17:25:57 -0400 Subject: [PATCH 2/3] simplify updateKeyedEach --- src/shared/keyed-each.js | 116 ++++++++---------- .../_config.js | 62 ++++++---- 2 files changed, 87 insertions(+), 91 deletions(-) diff --git a/src/shared/keyed-each.js b/src/shared/keyed-each.js index fd9ad18c59..d1999d4672 100644 --- a/src/shared/keyed-each.js +++ b/src/shared/keyed-each.js @@ -1,92 +1,78 @@ import { assign } from './utils.js'; -export function updateKeyedEach(component, key, changed, block_key, dynamic, each_block_value, head, lookup, updateMountNode, hasOutroMethod, create_each_block, get_context) { - var last = null; - var expected = head; +export function destroyIteration(iteration, lookup) { + var first = iteration.first + if (first && first.parentNode) { + iteration.u(); + } + iteration.d(); + lookup[iteration.key] = null; +} + +export function outroAndDestroyIteration(iteration, lookup) { + iteration.o(function() { + iteration.u(); + iteration.d(); + lookup[iteration.key] = null; + }); +} +export function updateKeyedEach(component, key, changed, key_prop, dynamic, list, head, lookup, updateMountNode, hasOutroMethod, create_each_block, get_context) { var keep = {}; - var mounts = {}; - var next_iteration = null; - for (i = 0; i < each_block_value.length; i += 1) { - var key = each_block_value[i][block_key]; + var i = list.length; + while (i--) { + var key = list[i][key_prop]; var iteration = lookup[key]; - var next_data = each_block_value[i+1]; - var next = next_data && lookup[next_data[block_key]]; - if (dynamic && iteration) iteration.p(changed, get_context(i)); // TODO should this be deferred? could it be redundant? - - if (expected && (key === expected.key)) { - var first = iteration && iteration.first; - var parentNode = first && first.parentNode - if (!parentNode || (iteration && iteration.next) != next) mounts[key] = iteration; - expected = iteration.next; - } else if (iteration) { - mounts[key] = iteration; - expected = iteration.next; + if (iteration) { + if (dynamic) iteration.p(changed, get_context(i)); } else { - // key is being inserted iteration = lookup[key] = create_each_block(component, key, get_context(i)); iteration.c(); - mounts[key] = iteration; } + lookup[key] = iteration; - keep[iteration.key] = iteration; - last = iteration; + keep[key] = 1; } var destroy = hasOutroMethod - ? function(iteration) { - iteration.o(function() { - iteration.u(); - iteration.d(); - lookup[iteration.key] = null; - }); - } - : function(iteration) { - var first = iteration.first - if (first && first.parentNode) { - iteration.u(); - } - iteration.d(); - lookup[iteration.key] = null; - } + ? outroAndDestroyIteration + : destroyIteration; iteration = head; - while(iteration) { - if (!keep[iteration.key]) { - destroy(iteration); - } + while (iteration) { + if (!keep[iteration.key]) destroy(iteration, lookup); iteration = iteration.next; } - // Work backwards due to DOM api having insertBefore - for (i = each_block_value.length - 1; i >= 0; i -= 1) { - var data = each_block_value[i]; - var key = data[block_key]; + var next = null; + + i = list.length; + while (i--) { + key = list[i][key_prop]; iteration = lookup[key]; - var block = mounts[key]; - if (block) { - var anchor; + var anchor; - if (hasOutroMethod) { - var key_next_iteration = next_iteration && next_iteration.key; - var iteration_anchor = iteration.next; - var key_anchor; - do { - anchor = iteration_anchor && iteration_anchor.first; - iteration_anchor = iteration_anchor && iteration_anchor.next; - key_anchor = iteration_anchor && iteration_anchor.key; - } while(iteration_anchor && key_anchor != key_next_iteration && !keep[key_anchor]) - } else { - anchor = next_iteration && next_iteration.first; - } + if (hasOutroMethod) { + var next_key = next && next.key; + var neighbour = iteration.next; + var anchor_key; - block[block.i ? 'i' : 'm'](updateMountNode, anchor); + 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; } - iteration.next = next_iteration; - if (next_iteration) next_iteration.last = iteration; - next_iteration = iteration; + + iteration[iteration.i ? 'i' : 'm'](updateMountNode, anchor); + + iteration.next = next; + if (next) next.last = iteration; + next = iteration; } } \ No newline at end of file diff --git a/test/runtime/samples/each-block-keyed-random-permute/_config.js b/test/runtime/samples/each-block-keyed-random-permute/_config.js index 904208c6a9..164257056f 100644 --- a/test/runtime/samples/each-block-keyed-random-permute/_config.js +++ b/test/runtime/samples/each-block-keyed-random-permute/_config.js @@ -1,50 +1,60 @@ -const VALUES = Array.from( 'abcdefghijklmnopqrstuvwxyz' ); +const VALUES = Array.from('abcdefghijklmnopqrstuvwxyz'); -function toObjects ( array ) { - return array.split( '' ).map( x => ({ id: x }) ); +function toObjects(array) { + return array.split('').map(x => ({ id: x })); } -function permute () { +function permute() { const values = VALUES.slice(); const number = Math.floor(Math.random() * VALUES.length); const permuted = []; for (let i = 0; i < number; i++) { - permuted.push( ...values.splice( Math.floor( Math.random() * ( number - i ) ), 1 ) ); + permuted.push( + ...values.splice(Math.floor(Math.random() * (number - i)), 1) + ); } - return permuted.join( '' ); + return permuted.join(''); } export default { data: { - values: toObjects( 'abc' ) + values: toObjects('abc'), }, html: `(a)(b)(c)`, - test ( assert, component, target ) { - function test ( sequence ) { - component.set({ values: toObjects( sequence ) }); - assert.htmlEqual( target.innerHTML, sequence.split( '' ).map( x => `(${x})` ).join( '' ) ); + test(assert, component, target) { + function test(sequence) { + const previous = target.textContent; + const expected = sequence.split('').map(x => `(${x})`).join(''); + component.set({ values: toObjects(sequence) }); + assert.htmlEqual( + target.innerHTML, + expected, + `\n${previous} -> ${expected}\n${target.textContent}` + ); } // first, some fixed tests so that we can debug them - test( 'abc' ); - test( 'abcd' ); - test( 'abecd' ); - test( 'fabecd' ); - test( 'fabed' ); - test( 'beadf' ); - test( 'ghbeadf' ); - test( 'gf' ); - test( 'gc' ); - test( 'g' ); - test( '' ); - test( 'abc' ); - test( 'duqbmineapjhtlofrskcg' ); - test( 'hdnkjougmrvftewsqpailcb' ); + test('abc'); + test('abcd'); + test('abecd'); + test('fabecd'); + test('fabed'); + test('beadf'); + test('ghbeadf'); + test('gf'); + test('gc'); + test('g'); + test(''); + test('abc'); + test('duqbmineapjhtlofrskcg'); + test('hdnkjougmrvftewsqpailcb'); + test('bidhfacge'); + test('kgjnempcboaflidh'); // then, we party - for ( let i = 0; i < 100; i += 1 ) test( permute() ); + for (let i = 0; i < 100; i += 1) test(permute()); } }; From de4e3a3c51cae81bb5a9961813b2361ac6bff452 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 18 Mar 2018 18:52:42 -0400 Subject: [PATCH 3/3] tweaks --- src/generators/nodes/EachBlock.ts | 2 +- src/shared/keyed-each.js | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/generators/nodes/EachBlock.ts b/src/generators/nodes/EachBlock.ts index ffc95eb485..ba9ffc11a9 100644 --- a/src/generators/nodes/EachBlock.ts +++ b/src/generators/nodes/EachBlock.ts @@ -319,7 +319,7 @@ export default class EachBlock extends Node { 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}, function(#i) { + @updateKeyedEach(#component, ${key}, changed, "${this.key}", ${dynamic}, ${each_block_value}, ${head}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", function(#i) { return @assign({}, state, { ${this.contextProps.join(',\n')} }); diff --git a/src/shared/keyed-each.js b/src/shared/keyed-each.js index d1999d4672..384a96f6ed 100644 --- a/src/shared/keyed-each.js +++ b/src/shared/keyed-each.js @@ -17,7 +17,9 @@ export function outroAndDestroyIteration(iteration, lookup) { }); } -export function updateKeyedEach(component, key, changed, key_prop, dynamic, list, head, lookup, updateMountNode, hasOutroMethod, create_each_block, get_context) { +// 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 = {}; var i = list.length; @@ -36,7 +38,7 @@ export function updateKeyedEach(component, key, changed, key_prop, dynamic, list keep[key] = 1; } - var destroy = hasOutroMethod + var destroy = has_outro ? outroAndDestroyIteration : destroyIteration; @@ -55,7 +57,7 @@ export function updateKeyedEach(component, key, changed, key_prop, dynamic, list var anchor; - if (hasOutroMethod) { + if (has_outro) { var next_key = next && next.key; var neighbour = iteration.next; var anchor_key; @@ -69,7 +71,7 @@ export function updateKeyedEach(component, key, changed, key_prop, dynamic, list anchor = next && next.first; } - iteration[iteration.i ? 'i' : 'm'](updateMountNode, anchor); + iteration[intro_method](node, anchor); iteration.next = next; if (next) next.last = iteration;