first crack at reactive declarations

pull/1839/head
Rich Harris 7 years ago
parent 7e72b49b6e
commit a390797c4d

@ -20,6 +20,7 @@ import isReference from 'is-reference';
import TemplateScope from './nodes/shared/TemplateScope'; import TemplateScope from './nodes/shared/TemplateScope';
import fuzzymatch from '../utils/fuzzymatch'; import fuzzymatch from '../utils/fuzzymatch';
import { remove_indentation } from '../utils/remove_indentation'; import { remove_indentation } from '../utils/remove_indentation';
import getObject from '../utils/getObject';
type Meta = { type Meta = {
namespace?: string; namespace?: string;
@ -82,6 +83,8 @@ export default class Component {
module_exports: Array<{ name: string, as: string }> = []; module_exports: Array<{ name: string, as: string }> = [];
partly_hoisted: string[] = []; partly_hoisted: string[] = [];
fully_hoisted: string[] = []; fully_hoisted: string[] = [];
reactive_declarations: Array<{ assignees: Set<string>, dependencies: Set<string>, snippet: string }> = [];
reactive_declaration_nodes: Set<Node> = new Set();
has_reactive_assignments = false; has_reactive_assignments = false;
indirectDependencies: Map<string, Set<string>> = new Map(); indirectDependencies: Map<string, Set<string>> = new Map();
@ -428,6 +431,7 @@ export default class Component {
extract_javascript(script) { extract_javascript(script) {
const nodes_to_include = script.content.body.filter(node => { const nodes_to_include = script.content.body.filter(node => {
if (this.hoistable_nodes.has(node)) return false; if (this.hoistable_nodes.has(node)) return false;
if (this.reactive_declaration_nodes.has(node)) return false;
if (node.type === 'ImportDeclaration') return false; if (node.type === 'ImportDeclaration') return false;
if (node.type === 'ExportDeclaration' && node.specifiers.length > 0) return false; if (node.type === 'ExportDeclaration' && node.specifiers.length > 0) return false;
return true; return true;
@ -440,10 +444,14 @@ export default class Component {
let result = ''; let result = '';
script.content.body.forEach(node => { script.content.body.forEach((node, i) => {
if (this.hoistable_nodes.has(node)) { if (this.hoistable_nodes.has(node) || this.reactive_declaration_nodes.has(node)) {
result += `[✂${a}-${node.start}✂]/* HOISTED */`; result += `[✂${a}-${node.start}✂]`;
a = node.end; a = node.end;
if (i < script.content.body.length - 1) {
while (a < this.source.length && /\s/.test(this.source[a])) a += 1;
}
} }
}); });
@ -498,6 +506,7 @@ export default class Component {
this.extract_imports_and_exports(script.content, this.imports, this.props); this.extract_imports_and_exports(script.content, this.imports, this.props);
this.rewrite_props(); this.rewrite_props();
this.hoist_instance_declarations(); this.hoist_instance_declarations();
this.extract_reactive_declarations();
this.javascript = this.extract_javascript(script); this.javascript = this.extract_javascript(script);
} }
@ -739,6 +748,111 @@ export default class Component {
} }
} }
extract_reactive_declarations() {
const component = this;
const unsorted_reactive_declarations = [];
this.instance_script.content.body.forEach(node => {
if (node.type === 'LabeledStatement') {
this.reactive_declaration_nodes.add(node);
const assignees = new Set();
const dependencies = new Set();
let scope = this.instance_scope;
let map = this.instance_scope_map;
walk(node.body, {
enter(node, parent) {
if (map.has(node)) {
scope = map.get(node);
}
if (parent && parent.type === 'AssignmentExpression' && node === parent.left) {
return this.skip();
}
if (node.type === 'AssignmentExpression') {
assignees.add(getObject(node.left).name);
} else if (node.type === 'UpdateExpression') {
assignees.add(getObject(node.argument).name);
this.skip();
} else if (isReference(node, parent)) {
const { name } = getObject(node);
if (component.declarations.indexOf(name) !== -1) {
dependencies.add(name);
}
this.skip();
}
},
leave(node) {
if (map.has(node)) {
scope = scope.parent;
}
}
});
unsorted_reactive_declarations.push({
assignees,
dependencies,
node,
snippet: node.body.type === 'BlockStatement'
? `[✂${node.body.start}-${node.end}✂]`
: `{ [✂${node.body.start}-${node.end}✂] }`
});
}
});
const lookup = new Map();
let seen;
unsorted_reactive_declarations.forEach(declaration => {
declaration.assignees.forEach(name => {
if (!lookup.has(name)) {
lookup.set(name, []);
}
// TODO warn or error if a name is assigned to in
// multiple reactive declarations?
lookup.get(name).push(declaration);
});
});
const add_declaration = declaration => {
if (seen.has(declaration)) {
this.error(declaration.node, {
code: 'cyclical-reactive-declaration',
message: 'Cyclical dependency detected'
});
}
seen.add(declaration);
if (declaration.dependencies.size === 0) {
this.error(declaration.node, {
code: 'invalid-reactive-declaration',
message: 'Invalid reactive declaration — must depend on local state'
});
}
declaration.dependencies.forEach(name => {
const earlier_declarations = lookup.get(name);
if (earlier_declarations) earlier_declarations.forEach(add_declaration);
});
if (this.reactive_declarations.indexOf(declaration) === -1) {
this.reactive_declarations.push(declaration);
}
};
unsorted_reactive_declarations.forEach(declaration => {
seen = new Set();
add_declaration(declaration);
});
}
qualify(name) { qualify(name) {
if (this.hoistable_names.has(name)) return name; if (this.hoistable_names.has(name)) return name;
if (this.imported_declarations.has(name)) return name; if (this.imported_declarations.has(name)) return name;

@ -7,6 +7,7 @@ import { CompileOptions } from '../../interfaces';
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import flattenReference from '../../utils/flattenReference'; import flattenReference from '../../utils/flattenReference';
import stringifyProps from '../../utils/stringifyProps'; import stringifyProps from '../../utils/stringifyProps';
import addToSet from '../../utils/addToSet';
export default function dom( export default function dom(
component: Component, component: Component,
@ -206,13 +207,19 @@ export default function dom(
component.javascript || component.javascript ||
filtered_props.length > 0 || filtered_props.length > 0 ||
component.partly_hoisted.length > 0 || component.partly_hoisted.length > 0 ||
filtered_declarations.length > 0 filtered_declarations.length > 0 ||
component.reactive_declarations.length > 0
); );
const definition = has_definition const definition = has_definition
? component.alias('define') ? component.alias('define')
: '@noop'; : '@noop';
const all_reactive_dependencies = new Set();
component.reactive_declarations.forEach(d => {
addToSet(all_reactive_dependencies, d.dependencies);
});
if (has_definition) { if (has_definition) {
builder.addBlock(deindent` builder.addBlock(deindent`
function ${definition}(${args.join(', ')}) { function ${definition}(${args.join(', ')}) {
@ -227,6 +234,14 @@ export default function dom(
${set && `$$self.$$.set = ${set};`} ${set && `$$self.$$.set = ${set};`}
${component.reactive_declarations.length > 0 && deindent`
$$self.$$.update = ($$dirty = { ${Array.from(all_reactive_dependencies).map(n => `${n}: 1`).join(', ')} }) => {
${component.reactive_declarations.map(d => deindent`
if (${Array.from(d.dependencies).map(n => `$$dirty.${n}`).join(' || ')}) ${d.snippet}
`)}
};
`}
${inject_refs && `$$self.$$.inject_refs = ${inject_refs};`} ${inject_refs && `$$self.$$.inject_refs = ${inject_refs};`}
} }
`); `);

@ -51,12 +51,17 @@ export default function ssr(
do { do {
$$settled = true; $$settled = true;
${component.reactive_declarations.map(d => d.snippet)}
$$rendered = \`${renderer.code}\`; $$rendered = \`${renderer.code}\`;
} while (!$$settled); } while (!$$settled);
return $$rendered; return $$rendered;
` `
: `return \`${renderer.code}\`;`; : deindent`
${component.reactive_declarations.map(d => d.snippet)}
return \`${renderer.code}\`;`;
const blocks = [ const blocks = [
setup, setup,

@ -74,6 +74,7 @@ export function init(component, options, define, create_fragment, not_equal) {
// state // state
get: empty, get: empty,
set: noop, set: noop,
update: noop,
inject_refs: noop, inject_refs: noop,
not_equal, not_equal,
bound: blankObject(), bound: blankObject(),
@ -97,6 +98,7 @@ export function init(component, options, define, create_fragment, not_equal) {
if (component.$$.bound[key]) component.$$.bound[key](component.$$.get()[key]); if (component.$$.bound[key]) component.$$.bound[key](component.$$.get()[key]);
}); });
component.$$.update();
run_all(component.$$.before_render); run_all(component.$$.before_render);
component.$$.fragment = create_fragment(component, component.$$.get()); component.$$.fragment = create_fragment(component, component.$$.get());

@ -55,6 +55,7 @@ export function flush() {
function update($$) { function update($$) {
if ($$.fragment) { if ($$.fragment) {
$$.update($$.dirty);
run_all($$.before_render); run_all($$.before_render);
$$.fragment.p($$.dirty, $$.get()); $$.fragment.p($$.dirty, $$.get());
$$.inject_refs($$.refs); $$.inject_refs($$.refs);

@ -46,10 +46,6 @@ function foo(node, callback) {
function define($$self, $$props) { function define($$self, $$props) {
let { bar } = $$props; let { bar } = $$props;
/* HOISTED */
/* HOISTED */
function foo_function() { function foo_function() {
return handleFoo(bar); return handleFoo(bar);
} }

@ -28,11 +28,11 @@ function create_fragment(component, ctx) {
}, },
p: function update(changed, ctx) { p: function update(changed, ctx) {
if (text0_value !== (text0_value = Math.max(0, ctx.foo))) { if ((changed.foo) && text0_value !== (text0_value = Math.max(0, ctx.foo))) {
setData(text0, text0_value); setData(text0, text0_value);
} }
if ((changed.bar || changed.foo) && text2_value !== (text2_value = ctx.bar())) { if ((changed.bar) && text2_value !== (text2_value = ctx.bar())) {
setData(text2, text2_value); setData(text2, text2_value);
} }
}, },

@ -123,8 +123,6 @@ function foo(node, animation, params) {
function define($$self, $$props) { function define($$self, $$props) {
let { things } = $$props; let { things } = $$props;
/* HOISTED */
$$self.$$.get = () => ({ things }); $$self.$$.get = () => ({ things });
$$self.$$.set = $$props => { $$self.$$.set = $$props => {

@ -23,10 +23,6 @@ const SvelteComponent = create_ssr_component(($$result, $$props, $$bindings, $$s
console.log('onDestroy'); console.log('onDestroy');
}); });
/* HOISTED */
/* HOISTED */
return ``; return ``;
}); });

@ -45,7 +45,7 @@ export default {
inputs[1].checked = true; inputs[1].checked = true;
await inputs[1].dispatchEvent(event); await inputs[1].dispatchEvent(event);
assert.equal(component.numCompleted(), 2); assert.equal(component.numCompleted, 2);
assert.htmlEqual(target.innerHTML, ` assert.htmlEqual(target.innerHTML, `
<div><input type="checkbox"><p>one</p></div><div><input type="checkbox"><p>two</p></div><div><input type="checkbox"><p>three</p></div> <div><input type="checkbox"><p>one</p></div><div><input type="checkbox"><p>two</p></div><div><input type="checkbox"><p>three</p></div>
<p>2 completed</p> <p>2 completed</p>

@ -1,13 +1,14 @@
<script> <script>
export let items; export let items;
export let numCompleted;
export function numCompleted() { $: numCompleted = items.reduce((total, item) => {
return items.reduce((total, item) => total + (item.completed ? 1 : 0), 0); return total + (item.completed ? 1 : 0);
} }, 0);
</script> </script>
{#each items as item} {#each items as item}
<div><input type='checkbox' bind:checked={item.completed}><p>{item.description}</p></div> <div><input type='checkbox' bind:checked={item.completed}><p>{item.description}</p></div>
{/each} {/each}
<p>{numCompleted()} completed</p> <p>{numCompleted} completed</p>

@ -3,18 +3,17 @@
export let count; export let count;
export let idToValue = Object.create(null); export let idToValue = Object.create(null);
let ids;
function ids() { $: ids = new Array(count)
return new Array(count)
.fill(null) .fill(null)
.map((_, i) => 'id-' + i); .map((_, i) => 'id-' + i);
}
</script> </script>
<input type='number' bind:value={count}> <input type='number' bind:value={count}>
<ol> <ol>
{#each ids() as id} {#each ids as id}
<Nested {id} bind:value={idToValue[id]}> <Nested {id} bind:value={idToValue[id]}>
{id}: value is {idToValue[id]} {id}: value is {idToValue[id]}
</Nested> </Nested>

@ -2,18 +2,18 @@
export let currentIdentifier; export let currentIdentifier;
export let identifier; export let identifier;
function isCurrentlySelected() { let isCurrentlySelected;
return currentIdentifier === identifier;
}
function toggle() { function toggle() {
currentIdentifier = isCurrentlySelected() ? null : identifier currentIdentifier = isCurrentlySelected ? null : identifier
} }
$: isCurrentlySelected = currentIdentifier === identifier;
</script> </script>
<span <span
on:click="{toggle}" on:click="{toggle}"
class="{isCurrentlySelected() ? 'selected' : ''}" class="{isCurrentlySelected ? 'selected' : ''}"
> >
<slot></slot> <slot></slot>
</span> </span>

@ -3,12 +3,12 @@
export let range = [0, 100]; export let range = [0, 100];
export let x = 5; export let x = 5;
function scale() { let scale;
return num => {
$: scale = num => {
const t = domain[0] + (num - domain[0]) / (domain[1] - domain[0]); const t = domain[0] + (num - domain[0]) / (domain[1] - domain[0]);
return range[0] + t * (range[1] - range[0]); return range[0] + t * (range[1] - range[0]);
} };
}
</script> </script>
<p>{scale()(x)}</p> <p>{scale(x)}</p>

@ -1,9 +1,8 @@
<script> <script>
export let x = 'waiting'; export let x = 'waiting';
function state() { let state;
return x; $: state = x;
}
</script> </script>
<span>{state()}</span> <span>{state}</span>

@ -3,7 +3,7 @@ export default {
test({ assert, component, target }) { test({ assert, component, target }) {
component.y = 2; component.y = 2;
assert.equal(component.x(), 4); assert.equal(component.x, 4);
assert.equal(target.innerHTML, '<p>4</p>'); assert.equal(target.innerHTML, '<p>4</p>');
} }
}; };

@ -7,14 +7,15 @@
export let y = 1; export let y = 1;
function xGetter() { let xGetter;
export let x;
$: {
_x = y * 2; _x = y * 2;
return getX; xGetter = getX;
} }
export function x() { $: x = xGetter();
return xGetter()();
}
</script> </script>
<p>{x()}</p> <p>{x}</p>

@ -6,8 +6,8 @@ export default {
test({ assert, component, target }) { test({ assert, component, target }) {
component.a = 3; component.a = 3;
assert.equal(component.c(), 5); assert.equal(component.c, 5);
assert.equal(component.cSquared(), 25); assert.equal(component.cSquared, 25);
assert.htmlEqual(target.innerHTML, ` assert.htmlEqual(target.innerHTML, `
<p>3 + 2 = 5</p> <p>3 + 2 = 5</p>
<p>5 * 5 = 25</p> <p>5 * 5 = 25</p>

@ -1,13 +1,12 @@
<script> <script>
export let a = 1; export let a = 1;
export let b = 2; export let b = 2;
export let c;
export let cSquared;
export function c() { $: c = a + b;
return a + b; $: cSquared = c * c;
}
export const cSquared = () => c() * c();
</script> </script>
<p>{a} + {b} = {c()}</p> <p>{a} + {b} = {c}</p>
<p>{c()} * {c()} = {cSquared()}</p> <p>{c} * {c} = {cSquared}</p>
Loading…
Cancel
Save