diff --git a/src/compiler/compile/render_dom/Renderer.ts b/src/compiler/compile/render_dom/Renderer.ts index d6de9773307b..4e385ed917a8 100644 --- a/src/compiler/compile/render_dom/Renderer.ts +++ b/src/compiler/compile/render_dom/Renderer.ts @@ -6,6 +6,7 @@ import { x } from 'code-red'; import { Node, Identifier, MemberExpression, Literal, Expression, BinaryExpression } from 'estree'; import flatten_reference from '../utils/flatten_reference'; import { reserved_keywords } from '../utils/reserved_keywords'; +import { renderer_invalidate } from './invalidate'; interface ContextMember { name: string; @@ -168,57 +169,7 @@ export default class Renderer { } invalidate(name: string, value?, main_execution_context: boolean = false) { - const variable = this.component.var_lookup.get(name); - const member = this.context_lookup.get(name); - - if (variable && (variable.subscribable && (variable.reassigned || variable.export_name))) { - return main_execution_context - ? x`${`$$subscribe_${name}`}(${value || name})` - : x`${`$$subscribe_${name}`}($$invalidate(${member.index}, ${value || name}))`; - } - - if (name[0] === '$' && name[1] !== '$') { - return x`${name.slice(1)}.set(${value || name})`; - } - - if ( - variable && ( - variable.module || ( - !variable.referenced && - !variable.is_reactive_dependency && - !variable.export_name && - !name.startsWith('$$') - ) - ) - ) { - return value || name; - } - - if (value) { - return x`$$invalidate(${member.index}, ${value})`; - } - - // if this is a reactive declaration, invalidate dependencies recursively - const deps = new Set([name]); - - deps.forEach(name => { - const reactive_declarations = this.component.reactive_declarations.filter(x => - x.assignees.has(name) - ); - reactive_declarations.forEach(declaration => { - declaration.dependencies.forEach(name => { - deps.add(name); - }); - }); - }); - - // TODO ideally globals etc wouldn't be here in the first place - const filtered = Array.from(deps).filter(n => this.context_lookup.has(n)); - if (!filtered.length) return null; - - return filtered - .map(n => x`$$invalidate(${this.context_lookup.get(n).index}, ${n})`) - .reduce((lhs, rhs) => x`${lhs}, ${rhs}`); + return renderer_invalidate(this, name, value, main_execution_context); } dirty(names: string[], is_reactive_declaration = false): Expression { diff --git a/src/compiler/compile/render_dom/invalidate.ts b/src/compiler/compile/render_dom/invalidate.ts index f88c566d96b5..1ea3c95b956d 100644 --- a/src/compiler/compile/render_dom/invalidate.ts +++ b/src/compiler/compile/render_dom/invalidate.ts @@ -33,7 +33,7 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names: if (main_execution_context && !variable.subscribable && variable.name[0] !== '$') { return node; } - return renderer.invalidate(variable.name, undefined, main_execution_context); + return renderer_invalidate(renderer, variable.name, undefined, main_execution_context); } if (!head) { @@ -79,3 +79,66 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names: return invalidate; } + +export function renderer_invalidate(renderer: Renderer, name: string, value?, main_execution_context: boolean = false) { + const variable = renderer.component.var_lookup.get(name); + + if (variable && (variable.subscribable && (variable.reassigned || variable.export_name))) { + if (main_execution_context) { + return x`${`$$subscribe_${name}`}(${value || name})`; + } else { + const member = renderer.context_lookup.get(name); + return x`${`$$subscribe_${name}`}($$invalidate(${member.index}, ${value || name}))`; + } + } + + if (name[0] === '$' && name[1] !== '$') { + return x`${name.slice(1)}.set(${value || name})`; + } + + if ( + variable && ( + variable.module || ( + !variable.referenced && + !variable.is_reactive_dependency && + !variable.export_name && + !name.startsWith('$$') + ) + ) + ) { + return value || name; + } + + if (value) { + if (main_execution_context) { + return x`${value}`; + } else { + const member = renderer.context_lookup.get(name); + return x`$$invalidate(${member.index}, ${value})`; + } + } + + if (main_execution_context) return; + + // if this is a reactive declaration, invalidate dependencies recursively + const deps = new Set([name]); + + deps.forEach(name => { + const reactive_declarations = renderer.component.reactive_declarations.filter(x => + x.assignees.has(name) + ); + reactive_declarations.forEach(declaration => { + declaration.dependencies.forEach(name => { + deps.add(name); + }); + }); + }); + + // TODO ideally globals etc wouldn't be here in the first place + const filtered = Array.from(deps).filter(n => renderer.context_lookup.has(n)); + if (!filtered.length) return null; + + return filtered + .map(n => x`$$invalidate(${renderer.context_lookup.get(n).index}, ${n})`) + .reduce((lhs, rhs) => x`${lhs}, ${rhs}`); +} \ No newline at end of file diff --git a/src/compiler/compile/render_ssr/index.ts b/src/compiler/compile/render_ssr/index.ts index b395a5973744..c9971306c454 100644 --- a/src/compiler/compile/render_ssr/index.ts +++ b/src/compiler/compile/render_ssr/index.ts @@ -8,6 +8,7 @@ import Text from '../nodes/Text'; import { LabeledStatement, Statement, Node, Expression } from 'estree'; import { walk } from 'estree-walker'; import { extract_names } from 'periscopic'; +import { invalidate } from '../render_dom/invalidate'; export default function ssr( component: Component, @@ -40,19 +41,36 @@ export default function ssr( const slots = uses_slots ? b`let $$slots = @compute_slots(#slots);` : null; const reactive_stores = component.vars.filter(variable => variable.name[0] === '$' && variable.name[1] !== '$'); - const reactive_store_values = reactive_stores + const reactive_store_subscriptions = reactive_stores + .filter(store => { + const variable = component.var_lookup.get(store.name.slice(1)); + return !variable || variable.hoistable; + }) + .map(({ name }) => { + const store_name = name.slice(1); + return b` + ${component.compile_options.dev && b`@validate_store(${store_name}, '${store_name}');`} + ${`$$unsubscribe_${store_name}`} = @subscribe(${store_name}, #value => ${name} = #value) + ${store_name}.subscribe($$value => ${name} = $$value); + ` + }); + const reactive_store_unsubscriptions = reactive_stores.map( + ({ name }) => b`${`$$unsubscribe_${name.slice(1)}`}()` + ); + + const reactive_store_declarations = reactive_stores .map(({ name }) => { const store_name = name.slice(1); const store = component.var_lookup.get(store_name); - if (store && store.hoistable) return null; - const assignment = b`${name} = @get_store_value(${store_name});`; + if (store && store.reassigned) { + const unsubscribe = `$$unsubscribe_${store_name}`; + const subscribe = `$$subscribe_${store_name}`; - return component.compile_options.dev - ? b`@validate_store(${store_name}, '${store_name}'); ${assignment}` - : assignment; - }) - .filter(Boolean); + return b`let ${name}, ${unsubscribe} = @noop, ${subscribe} = () => (${unsubscribe}(), ${unsubscribe} = @subscribe(${store_name}, $$value => ${name} = $$value), ${store_name})`; + } + return b`let ${name}, ${`$$unsubscribe_${store_name}`};`; + }); // instrument get/set store value if (component.ast.instance) { @@ -72,51 +90,45 @@ export default function ssr( if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') { const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument; - const names = new Set(extract_names(assignee)); - - const vars = Array.from(names) - .filter(name => { - const owner = scope.find_owner(name); - return !owner || owner === component.instance_scope; - }) - .map(name => component.var_lookup.get(name)) - .filter(variable => { - return variable && - !variable.hoistable && - !variable.global && - !variable.module && - ( - variable.subscribable || variable.name[0] === '$' - ); - }) - .map(variable => { - if (variable.subscribable) { - return x`${'$' + variable.name} = @get_store_value(${variable.name})`; + let names = new Set(extract_names(assignee)); + let to_invalidate = new Set(); + + for (const name of names) { + const variable = component.var_lookup.get(name); + if (variable && + !variable.hoistable && + !variable.global && + !variable.module && + ( + variable.subscribable || variable.name[0] === '$' + )) { + to_invalidate.add(variable.name); } - if (variable.name[0] === '$') { - return x`${variable.name.slice(1)}.set(${variable.name})`; - } - }); - - if (vars.length) { - this.replace({ - type: 'SequenceExpression', - expressions: [ - node, - ...vars, - assignee as Expression - ] - }); + } + + if (to_invalidate.size) { + this.replace( + invalidate( + { component } as any, + scope, + node, + to_invalidate, + true + ) + ); } } } }); } - component.rewrite_props(({ name }) => { + component.rewrite_props(({ name, reassigned }) => { const value = `$${name}`; - let insert = b`${value} = @get_store_value(${name})`; + let insert = reassigned + ? b`${`$$subscribe_${name}`}()` + : b`${`$$unsubscribe_${name}`} = @subscribe(${name}, #value => $${value} = #value)`; + if (component.compile_options.dev) { insert = b`@validate_store(${name}, '${name}'); ${insert}`; } @@ -160,8 +172,6 @@ export default function ssr( do { $$settled = true; - ${reactive_store_values} - ${injected.map(name => b`let ${name};`)} ${reactive_declarations} @@ -169,32 +179,24 @@ export default function ssr( $$rendered = ${literal}; } while (!$$settled); + ${reactive_store_unsubscriptions} + return $$rendered; ` : b` - ${reactive_store_values} - ${injected.map(name => b`let ${name};`)} ${reactive_declarations} + ${reactive_store_unsubscriptions} + return ${literal};`; const blocks = [ rest, slots, - ...reactive_stores.map(({ name }) => { - const store_name = name.slice(1); - const store = component.var_lookup.get(store_name); - if (store && store.hoistable) { - return b` - ${component.compile_options.dev && b`@validate_store(${store_name}, '${store_name}');`} - let ${name} = @get_store_value(${store_name}); - `; - } - return b`let ${name};`; - }), - + ...reactive_store_declarations, + ...reactive_store_subscriptions, instance_javascript, ...parent_bindings, css.code && b`$$result.css.add(#css);`, diff --git a/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store-2/_config.js b/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store-2/_config.js index a22aa878ffae..62f47a01c3ef 100644 --- a/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store-2/_config.js +++ b/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store-2/_config.js @@ -1,6 +1,5 @@ // destructure to store value export default { - skip_if_ssr: true, // pending https://github.com/sveltejs/svelte/issues/3582 html: '

2 2 xxx 5 6 9 10 2

', async test({ assert, target, component }) { await component.update(); diff --git a/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store/_config.js b/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store/_config.js index 6a613f73c4aa..d235d2a1a066 100644 --- a/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store/_config.js +++ b/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store/_config.js @@ -1,7 +1,6 @@ // destructure to store export default { html: '

2 2 xxx 5 6 9 10 2

', - skip_if_ssr: true, async test({ assert, target, component }) { await component.update(); assert.htmlEqual(target.innerHTML, '

11 11 yyy 12 13 14 15 11

'); diff --git a/test/runtime/samples/store-auto-resubscribe-immediate/main.svelte b/test/runtime/samples/store-auto-resubscribe-immediate/main.svelte index 10721c4b4957..2ad423feba91 100644 --- a/test/runtime/samples/store-auto-resubscribe-immediate/main.svelte +++ b/test/runtime/samples/store-auto-resubscribe-immediate/main.svelte @@ -7,10 +7,10 @@ // should resubscribe immediately value = writable({ foo: $value.foo + 2, bar: $value.bar - 2 }); // { foo: 5, bar: 4 } - + // should mutate the store value $value.baz = $value.foo + $value.bar; // { foo: 5, bar: 4, baz: 9 } - + // should resubscribe immediately value = writable({ qux: $value.baz - $value.foo }); // { qux: 4 }