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 fuzzymatch from '../utils/fuzzymatch';
import { remove_indentation } from '../utils/remove_indentation';
import getObject from '../utils/getObject';
type Meta = {
namespace?: string;
@ -82,6 +83,8 @@ export default class Component {
module_exports: Array<{ name: string, as: string }> = [];
partly_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;
indirectDependencies: Map<string, Set<string>> = new Map();
@ -428,6 +431,7 @@ export default class Component {
extract_javascript(script) {
const nodes_to_include = script.content.body.filter(node => {
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 === 'ExportDeclaration' && node.specifiers.length > 0) return false;
return true;
@ -440,10 +444,14 @@ export default class Component {
let result = '';
script.content.body.forEach(node => {
if (this.hoistable_nodes.has(node)) {
result += `[✂${a}-${node.start}✂]/* HOISTED */`;
script.content.body.forEach((node, i) => {
if (this.hoistable_nodes.has(node) || this.reactive_declaration_nodes.has(node)) {
result += `[✂${a}-${node.start}✂]`;
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.rewrite_props();
this.hoist_instance_declarations();
this.extract_reactive_declarations();
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) {
if (this.hoistable_names.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 flattenReference from '../../utils/flattenReference';
import stringifyProps from '../../utils/stringifyProps';
import addToSet from '../../utils/addToSet';
export default function dom(
component: Component,
@ -206,13 +207,19 @@ export default function dom(
component.javascript ||
filtered_props.length > 0 ||
component.partly_hoisted.length > 0 ||
filtered_declarations.length > 0
filtered_declarations.length > 0 ||
component.reactive_declarations.length > 0
);
const definition = has_definition
? component.alias('define')
: '@noop';
const all_reactive_dependencies = new Set();
component.reactive_declarations.forEach(d => {
addToSet(all_reactive_dependencies, d.dependencies);
});
if (has_definition) {
builder.addBlock(deindent`
function ${definition}(${args.join(', ')}) {
@ -227,6 +234,14 @@ export default function dom(
${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};`}
}
`);

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

@ -74,6 +74,7 @@ export function init(component, options, define, create_fragment, not_equal) {
// state
get: empty,
set: noop,
update: noop,
inject_refs: noop,
not_equal,
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]);
});
component.$$.update();
run_all(component.$$.before_render);
component.$$.fragment = create_fragment(component, component.$$.get());

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

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

@ -28,11 +28,11 @@ function create_fragment(component, 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);
}
if ((changed.bar || changed.foo) && text2_value !== (text2_value = ctx.bar())) {
if ((changed.bar) && text2_value !== (text2_value = ctx.bar())) {
setData(text2, text2_value);
}
},

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

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

@ -45,7 +45,7 @@ export default {
inputs[1].checked = true;
await inputs[1].dispatchEvent(event);
assert.equal(component.numCompleted(), 2);
assert.equal(component.numCompleted, 2);
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>
<p>2 completed</p>

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

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

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

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

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

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

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

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

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