instrument event handlers

pull/1864/head
Rich Harris 7 years ago
parent 6948f9f60e
commit 1f0345644f

3
.gitignore vendored

@ -17,4 +17,5 @@ node_modules
/src/compile/shared.ts /src/compile/shared.ts
/store.umd.js /store.umd.js
/yarn-error.log /yarn-error.log
_actual*.* _actual*.*
_

@ -96,11 +96,12 @@ export default class Stats {
} }
}); });
// TODO
const hooks: Record<string, boolean> = component && { const hooks: Record<string, boolean> = component && {
oncreate: !!component.templateProperties.oncreate, oncreate: false,
ondestroy: !!component.templateProperties.ondestroy, ondestroy: false,
onstate: !!component.templateProperties.onstate, onstate: false,
onupdate: !!component.templateProperties.onupdate onupdate: false
}; };
return { return {

@ -10,7 +10,7 @@ import namespaces from '../utils/namespaces';
import { removeNode } from '../utils/removeNode'; import { removeNode } from '../utils/removeNode';
import nodeToString from '../utils/nodeToString'; import nodeToString from '../utils/nodeToString';
import wrapModule from './wrapModule'; import wrapModule from './wrapModule';
import { createScopes, extractNames } from '../utils/annotateWithScopes'; import { createScopes, extractNames, Scope } from '../utils/annotateWithScopes';
import getName from '../utils/getName'; import getName from '../utils/getName';
import Stylesheet from './css/Stylesheet'; import Stylesheet from './css/Stylesheet';
import { test } from '../config'; import { test } from '../config';
@ -24,6 +24,7 @@ import checkForDupes from './validate/js/utils/checkForDupes';
import propValidators from './validate/js/propValidators'; import propValidators from './validate/js/propValidators';
import fuzzymatch from './validate/utils/fuzzymatch'; import fuzzymatch from './validate/utils/fuzzymatch';
import flattenReference from '../utils/flattenReference'; import flattenReference from '../utils/flattenReference';
import { instrument } from '../utils/instrument';
interface Computation { interface Computation {
key: string; key: string;
@ -102,6 +103,7 @@ export default class Component {
name: string; name: string;
options: CompileOptions; options: CompileOptions;
fragment: Fragment; fragment: Fragment;
scope: Scope;
meta: { meta: {
namespace?: string; namespace?: string;
@ -124,24 +126,13 @@ export default class Component {
animations: Set<string>; animations: Set<string>;
transitions: Set<string>; transitions: Set<string>;
actions: Set<string>; actions: Set<string>;
importedComponents: Map<string, string>;
namespace: string; namespace: string;
hasComponents: boolean; hasComponents: boolean;
computations: Computation[];
templateProperties: Record<string, Node>;
javascript: string; javascript: string;
used: {
components: Set<string>;
helpers: Set<string>;
events: Set<string>;
animations: Set<string>;
transitions: Set<string>;
actions: Set<string>;
};
declarations: string[]; declarations: string[];
exports: Array<{ name: string, as: string }>; exports: Array<{ name: string, as: string }>;
event_handlers: Array<{ name: string, body: string }>;
props: string[]; props: string[];
refCallees: Node[]; refCallees: Node[];
@ -189,19 +180,10 @@ export default class Component {
this.animations = new Set(); this.animations = new Set();
this.transitions = new Set(); this.transitions = new Set();
this.actions = new Set(); this.actions = new Set();
this.importedComponents = new Map();
this.used = {
components: new Set(),
helpers: new Set(),
events: new Set(),
animations: new Set(),
transitions: new Set(),
actions: new Set(),
};
this.declarations = []; this.declarations = [];
this.exports = []; this.exports = [];
this.event_handlers = [];
this.refs = new Set(); this.refs = new Set();
this.refCallees = []; this.refCallees = [];
@ -230,8 +212,6 @@ export default class Component {
this.aliases = new Map(); this.aliases = new Map();
this.usedNames = new Set(); this.usedNames = new Set();
this.computations = [];
this.templateProperties = {};
this.properties = new Map(); this.properties = new Map();
this.walkJs(); this.walkJs();
@ -486,6 +466,7 @@ export default class Component {
}); });
let { scope, map, globals } = createScopes(js.content); let { scope, map, globals } = createScopes(js.content);
this.scope = scope;
scope.declarations.forEach(name => { scope.declarations.forEach(name => {
this.userVars.add(name); this.userVars.add(name);
@ -546,7 +527,7 @@ export default class Component {
const top_scope = scope; const top_scope = scope;
walk(js.content, { walk(js.content, {
enter(node) { enter: (node, parent) => {
if (map.has(node)) { if (map.has(node)) {
scope = map.get(node); scope = map.get(node);
} }
@ -555,15 +536,9 @@ export default class Component {
const { name } = flattenReference(node.left); const { name } = flattenReference(node.left);
if (scope.findOwner(name) === top_scope) { if (scope.findOwner(name) === top_scope) {
// TODO verify that it needs to be reactive, i.e. is this.instrument(node, parent, name);
// used in the template (and not just in an event
// handler or transition etc)
// TODO handle arrow function expressions etc
code.appendLeft(node.end, `; $$make_dirty('${name}')`);
} }
} }
}, },
leave(node) { leave(node) {
@ -581,6 +556,21 @@ export default class Component {
this.javascript = a !== b ? `[✂${a}-${b}✂]` : ''; this.javascript = a !== b ? `[✂${a}-${b}✂]` : '';
} }
instrument(node, parent, name) {
// TODO only make values reactive if they're used
// in the template
if (parent.type === 'ArrowFunctionExpression' && node === parent.body) {
// TODO don't do the $$result dance if this is an event handler
this.code.prependRight(node.start, `{ const $$result = `);
this.code.appendLeft(node.end, `; $$make_dirty('${name}'); return $$result; }`);
}
else {
this.code.appendLeft(node.end, `; $$make_dirty('${name}')`);
}
}
} }
type Meta = { type Meta = {

@ -11,8 +11,6 @@ export default class Action extends Node {
this.name = info.name; this.name = info.name;
component.used.actions.add(this.name);
this.expression = info.expression this.expression = info.expression
? new Expression(component, this, scope, info.expression) ? new Expression(component, this, scope, info.expression)
: null; : null;

@ -11,8 +11,6 @@ export default class Animation extends Node {
this.name = info.name; this.name = info.name;
component.used.animations.add(this.name);
if (parent.animation) { if (parent.animation) {
component.error(this, { component.error(this, {
code: `duplicate-animation`, code: `duplicate-animation`,

@ -4,6 +4,8 @@ import addToSet from '../../utils/addToSet';
import flattenReference from '../../utils/flattenReference'; import flattenReference from '../../utils/flattenReference';
import validCalleeObjects from '../../utils/validCalleeObjects'; import validCalleeObjects from '../../utils/validCalleeObjects';
import list from '../../utils/list'; import list from '../../utils/list';
import { createScopes } from '../../utils/annotateWithScopes';
import { walk } from 'estree-walker';
const validBuiltins = new Set(['set', 'fire', 'destroy']); const validBuiltins = new Set(['set', 'fire', 'destroy']);
@ -18,30 +20,51 @@ export default class EventHandler extends Node {
usesContext: boolean; usesContext: boolean;
usesEventObject: boolean; usesEventObject: boolean;
isCustomEvent: boolean; isCustomEvent: boolean;
shouldHoist: boolean;
insertionPoint: number; insertionPoint: number;
args: Expression[]; args: Expression[];
snippet: string; snippet: string;
constructor(component, parent, scope, info) { constructor(component, parent, template_scope, info) {
super(component, parent, scope, info); super(component, parent, template_scope, info);
this.name = info.name; this.name = info.name;
this.modifiers = new Set(info.modifiers); this.modifiers = new Set(info.modifiers);
component.used.events.add(this.name);
this.dependencies = new Set(); this.dependencies = new Set();
if (info.expression) { if (info.expression) {
this.expression = new Expression(component, parent, scope, info.expression); this.expression = new Expression(component, parent, template_scope, info.expression, true);
this.snippet = this.expression.snippet; this.snippet = this.expression.snippet;
let { scope, map } = createScopes(info.expression);
walk(this.expression, {
enter: (node, parent) => {
if (map.has(node)) {
scope = map.get(node);
}
if (node.type === 'AssignmentExpression') {
const { name } = flattenReference(node.left);
if (!scope.has(name)) {
component.instrument(node, parent, name);
}
}
},
leave(node) {
if (map.has(node)) {
scope = scope.parent;
}
}
});
} else { } else {
this.snippet = null; // TODO handle shorthand events here? this.snippet = null; // TODO handle shorthand events here?
} }
this.isCustomEvent = component.events.has(this.name); // TODO figure out what to do about custom events
this.shouldHoist = !this.isCustomEvent && parent.hasAncestor('EachBlock'); // this.isCustomEvent = component.events.has(this.name);
} }
} }

@ -26,8 +26,6 @@ export default class Transition extends Node {
}); });
} }
this.component.used.transitions.add(this.name);
this.expression = info.expression this.expression = info.expression
? new Expression(component, this, scope, info.expression) ? new Expression(component, this, scope, info.expression)
: null; : null;

@ -65,7 +65,7 @@ export default class Expression {
thisReferences: Array<{ start: number, end: number }>; thisReferences: Array<{ start: number, end: number }>;
constructor(component, parent, scope, info) { constructor(component, parent, scope, info, isEventHandler?: boolean) {
// TODO revert to direct property access in prod? // TODO revert to direct property access in prod?
Object.defineProperties(this, { Object.defineProperties(this, {
component: { component: {
@ -113,8 +113,6 @@ export default class Expression {
let object = node; let object = node;
while (object.type === 'MemberExpression') object = object.object; while (object.type === 'MemberExpression') object = object.object;
component.used.helpers.add(name);
const alias = component.templateVars.get(`helpers-${name}`); const alias = component.templateVars.get(`helpers-${name}`);
if (alias !== name) code.overwrite(object.start, object.end, alias); if (alias !== name) code.overwrite(object.start, object.end, alias);
return; return;
@ -122,7 +120,7 @@ export default class Expression {
expression.usesContext = true; expression.usesContext = true;
if (!isSynthetic) { if (!isSynthetic && !isEventHandler) {
// <option> value attribute could be synthetic — avoid double editing // <option> value attribute could be synthetic — avoid double editing
code.prependRight(node.start, key === 'key' && parent.shorthand code.prependRight(node.start, key === 'key' && parent.shorthand
? `${name}: ctx.` ? `${name}: ctx.`

@ -109,14 +109,20 @@ export default function dom(
} else { } else {
const refs = Array.from(component.refs); const refs = Array.from(component.refs);
const declarations = component.declarations.concat(
component.event_handlers.map(handler => handler.name)
);
builder.addBlock(deindent` builder.addBlock(deindent`
class ${name} extends @SvelteComponent { class ${name} extends @SvelteComponent {
$$init($$make_dirty) { $$init($$make_dirty) {
${component.javascript || component.exports.map(x => `let ${x.name};`)} ${component.javascript || component.exports.map(x => `let ${x.name};`)}
${component.event_handlers.map(handler => handler.body)}
return [ return [
// TODO only what's needed by the template // TODO only what's needed by the template
() => ({ ${(component.declarations).join(', ')} }), () => ({ ${declarations.join(', ')} }),
props => { props => {
// TODO only do this for export let|var // TODO only do this for export let|var
${(component.exports.map(name => ${(component.exports.map(name =>
@ -148,8 +154,6 @@ export default function dom(
`); `);
} }
const immutable = templateProperties.immutable ? templateProperties.immutable.value.value : options.immutable;
let result = builder.toString(); let result = builder.toString();
return component.generate(result, options, { return component.generate(result, options, {

@ -606,60 +606,43 @@ export default class ElementWrapper extends Wrapper {
const { component } = renderer; const { component } = renderer;
this.node.handlers.forEach(handler => { this.node.handlers.forEach(handler => {
const isCustomEvent = component.events.has(handler.name); const { isCustomEvent } = handler;
const target = handler.shouldHoist ? 'this' : this.var;
// get a name for the event handler that is globally unique // get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise // if hoisted, locally unique otherwise
const handlerName = (handler.shouldHoist ? component : block).getUniqueName( const handler_name = component.getUniqueName(
`${handler.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler` `${handler.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
); );
const component_name = block.alias('component'); // can't use #component, might be hoisted const component_name = block.alias('component'); // can't use #component, might be hoisted
// create the handler body
const handlerBody = deindent`
${handler.shouldHoist && (
handler.usesComponent || handler.usesContext
? `const { ${[handler.usesComponent && 'component', handler.usesContext && 'ctx'].filter(Boolean).join(', ')} } = ${target}._svelte;`
: null
)}
${handler.snippet ?
handler.snippet :
`${component_name}.fire("${handler.name}", event);`}
`;
if (isCustomEvent) { if (isCustomEvent) {
block.addVariable(handlerName); throw new Error(`TODO figure out custom events`);
block.addVariable(handler_name);
block.builders.hydrate.addBlock(deindent` block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${handler.name}.call(${component_name}, ${this.var}, function(event) { ${handler_name} = ctx.${handler.name}.call(${component_name}, ${this.var}, function(event) {
${handlerBody} ${handler.snippet}
}); });
`); `);
block.builders.destroy.addLine(deindent` block.builders.destroy.addLine(deindent`
${handlerName}.destroy(); ${handler_name}.destroy();
`); `);
} else { } else {
const modifiers = []; const modifiers = [];
if (handler.modifiers.has('preventDefault')) modifiers.push('event.preventDefault();'); if (handler.modifiers.has('preventDefault')) modifiers.push('event.preventDefault();');
if (handler.modifiers.has('stopPropagation')) modifiers.push('event.stopPropagation();'); if (handler.modifiers.has('stopPropagation')) modifiers.push('event.stopPropagation();');
const handlerFunction = deindent` component.event_handlers.push({
function ${handlerName}(event) { name: handler_name,
${modifiers} body: deindent`
(${handlerBody})(event); function ${handler_name}(event) {
} ${modifiers}
`; (${handler.snippet})(event);
}
if (handler.shouldHoist) { `
renderer.blocks.push(handlerFunction); });
} else {
block.builders.init.addBlock(handlerFunction);
}
const opts = ['passive', 'once', 'capture'].filter(mod => handler.modifiers.has(mod)); const opts = ['passive', 'once', 'capture'].filter(mod => handler.modifiers.has(mod));
if (opts.length) { if (opts.length) {
@ -668,19 +651,19 @@ export default class ElementWrapper extends Wrapper {
: `{ ${opts.map(opt => `${opt}: true`).join(', ')} }`; : `{ ${opts.map(opt => `${opt}: true`).join(', ')} }`;
block.builders.hydrate.addLine( block.builders.hydrate.addLine(
`@addListener(${this.var}, "${handler.name}", ${handlerName}, ${optString});` `@addListener(${this.var}, "${handler.name}", ctx.${handler_name}, ${optString});`
); );
block.builders.destroy.addLine( block.builders.destroy.addLine(
`@removeListener(${this.var}, "${handler.name}", ${handlerName}, ${optString});` `@removeListener(${this.var}, "${handler.name}", ctx.${handler_name}, ${optString});`
); );
} else { } else {
block.builders.hydrate.addLine( block.builders.hydrate.addLine(
`@addListener(${this.var}, "${handler.name}", ${handlerName});` `@addListener(${this.var}, "${handler.name}", ctx.${handler_name});`
); );
block.builders.destroy.addLine( block.builders.destroy.addLine(
`@removeListener(${this.var}, "${handler.name}", ${handlerName});` `@removeListener(${this.var}, "${handler.name}", ctx.${handler_name});`
); );
} }
} }

@ -47,36 +47,42 @@ export default class WindowWrapper extends Wrapper {
let usesState = handler.dependencies.size > 0; let usesState = handler.dependencies.size > 0;
const handlerName = block.getUniqueName(`onwindow${handler.name}`); const handler_name = block.getUniqueName(`onwindow${handler.name}`);
const handlerBody = deindent` const handler_body = deindent`
${usesState && `var ctx = #component.get();`} ${usesState && `var ctx = #component.get();`}
${handler.snippet} ${handler.snippet}
`; `;
if (isCustomEvent) { if (isCustomEvent) {
// TODO dry this out // TODO dry this out
block.addVariable(handlerName); block.addVariable(handler_name);
block.builders.hydrate.addBlock(deindent` block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${handler.name}.call(#component, window, function(event) { ${handler_name} = %events-${handler.name}.call(#component, window, function(event) {
(${handlerBody})(event); (${handler_body})(event);
}); });
`); `);
block.builders.destroy.addLine(deindent` block.builders.destroy.addLine(deindent`
${handlerName}.destroy(); ${handler_name}.destroy();
`); `);
} else { } else {
block.builders.init.addBlock(deindent` component.event_handlers.push({
function ${handlerName}(event) { name: handler_name,
(${handlerBody})(event); body: deindent`
} function ${handler_name}(event) {
window.addEventListener("${handler.name}", ${handlerName}); (${handler.snippet})(event);
`); }
`
});
block.builders.destroy.addBlock(deindent` block.builders.init.addLine(
window.removeEventListener("${handler.name}", ${handlerName}); `window.addEventListener("${handler.name}", ctx.${handler_name});`
`); );
block.builders.destroy.addLine(
`window.removeEventListener("${handler.name}", ctx.${handler_name});`
);
} }
}); });
@ -106,7 +112,7 @@ export default class WindowWrapper extends Wrapper {
const timeout = block.getUniqueName(`window_updating_timeout`); const timeout = block.getUniqueName(`window_updating_timeout`);
Object.keys(events).forEach(event => { Object.keys(events).forEach(event => {
const handlerName = block.getUniqueName(`onwindow${event}`); const handler_name = block.getUniqueName(`onwindow${event}`);
const props = events[event]; const props = events[event];
if (event === 'scroll') { if (event === 'scroll') {
@ -138,7 +144,7 @@ export default class WindowWrapper extends Wrapper {
}); });
} }
const handlerBody = deindent` const handler_body = deindent`
${event === 'scroll' && deindent` ${event === 'scroll' && deindent`
if (${lock}) return; if (${lock}) return;
${lock} = true; ${lock} = true;
@ -154,14 +160,14 @@ export default class WindowWrapper extends Wrapper {
`; `;
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
function ${handlerName}(event) { function ${handler_name}(event) {
${handlerBody} ${handler_body}
} }
window.addEventListener("${event}", ${handlerName}); window.addEventListener("${event}", ${handler_name});
`); `);
block.builders.destroy.addBlock(deindent` block.builders.destroy.addBlock(deindent`
window.removeEventListener("${event}", ${handlerName}); window.removeEventListener("${event}", ${handler_name});
`); `);
}); });
@ -189,15 +195,15 @@ export default class WindowWrapper extends Wrapper {
// another special case. (I'm starting to think these are all special cases.) // another special case. (I'm starting to think these are all special cases.)
if (bindings.online) { if (bindings.online) {
const handlerName = block.getUniqueName(`onlinestatuschanged`); const handler_name = block.getUniqueName(`onlinestatuschanged`);
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
function ${handlerName}(event) { function ${handler_name}(event) {
${component.options.dev && `component._updatingReadonlyProperty = true;`} ${component.options.dev && `component._updatingReadonlyProperty = true;`}
#component.set({ ${bindings.online}: navigator.onLine }); #component.set({ ${bindings.online}: navigator.onLine });
${component.options.dev && `component._updatingReadonlyProperty = false;`} ${component.options.dev && `component._updatingReadonlyProperty = false;`}
} }
window.addEventListener("online", ${handlerName}); window.addEventListener("online", ${handler_name});
window.addEventListener("offline", ${handlerName}); window.addEventListener("offline", ${handler_name});
`); `);
// add initial value // add initial value
@ -206,8 +212,8 @@ export default class WindowWrapper extends Wrapper {
); );
block.builders.destroy.addBlock(deindent` block.builders.destroy.addBlock(deindent`
window.removeEventListener("online", ${handlerName}); window.removeEventListener("online", ${handler_name});
window.removeEventListener("offline", ${handlerName}); window.removeEventListener("offline", ${handler_name});
`); `);
} }
} }

Loading…
Cancel
Save