claim static html elements using innerHTML instead of deopt to creating the nodes

revert code

update test case
pull/7426/head
tanhauhau 4 years ago
parent e2b9df1888
commit 0363be1a6d

@ -39,6 +39,7 @@ import compiler_errors from './compiler_errors';
import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore';
import check_enable_sourcemap from './utils/check_enable_sourcemap';
import is_dynamic from './render_dom/wrappers/shared/is_dynamic';
import Tag from './nodes/shared/Tag';
interface ComponentOptions {
namespace?: string;
@ -111,6 +112,8 @@ export default class Component {
slots: Map<string, Slot> = new Map();
slot_outlets: Set<string> = new Set();
tags: Tag[] = []
constructor(
ast: Ast,
source: string,
@ -762,6 +765,7 @@ export default class Component {
this.hoist_instance_declarations();
this.extract_reactive_declarations();
this.check_if_tags_content_dynamic();
}
post_template_walk() {
@ -1492,6 +1496,12 @@ export default class Component {
unsorted_reactive_declarations.forEach(add_declaration);
}
check_if_tags_content_dynamic() {
this.tags.forEach(tag => {
tag.check_if_content_dynamic();
});
}
warn_if_undefined(name: string, node, template_scope: TemplateScope) {
if (name[0] === '$') {
if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) {

@ -59,6 +59,11 @@ export default class Attribute extends Node {
return expression;
});
}
if (this.dependencies.size > 0) {
parent.cannot_use_innerhtml();
parent.not_static_content();
}
}
get_dependencies() {

@ -27,6 +27,8 @@ export default class AwaitBlock extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression);

@ -33,6 +33,8 @@ export default class EachBlock extends AbstractBlock {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression);
this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring

@ -14,6 +14,7 @@ import map_children from './shared/map_children';
import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character } from '../../utils/patterns';
import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list';
import hash from '../utils/hash';
import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
@ -438,6 +439,24 @@ export default class Element extends Node {
this.optimise();
component.apply_stylesheet(this);
if (this.parent) {
if (this.actions.length > 0 ||
this.animation ||
this.bindings.length > 0 ||
this.classes.length > 0 ||
this.intro || this.outro ||
this.handlers.length > 0 ||
this.styles.length > 0 ||
this.name === 'option' ||
this.tag_expr.dynamic_dependencies().length ||
this.is_dynamic_element ||
component.compile_options.dev
) {
this.parent.cannot_use_innerhtml(); // need to use add_location
this.parent.not_static_content();
}
}
}
validate() {
@ -1140,6 +1159,20 @@ export default class Element extends Node {
}
});
}
get can_use_textcontent() {
return this.is_static_content && this.children.every(node => node.type === 'Text' || node.type === 'MustacheTag');
}
get can_optimise_to_html_string() {
const can_use_textcontent = this.can_use_textcontent;
const is_template_with_text_content = this.name === 'template' && can_use_textcontent;
return !is_template_with_text_content && !this.namespace && (this.can_use_innerhtml || can_use_textcontent) && this.children.length > 0;
}
hash() {
return `svelte-${hash(this.component.source.slice(this.start, this.end))}`;
}
}
const regex_starts_with_vowel = /^[aeiou]/;

@ -15,6 +15,8 @@ export default class Head extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.can_use_innerhtml = false;
if (info.attributes.length) {
component.error(info.attributes[0], compiler_errors.invalid_attribute_head);
return;

@ -18,6 +18,8 @@ export default class IfBlock extends AbstractBlock {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.scope = scope.child();
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, this.scope, info.expression);
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));

@ -28,6 +28,9 @@ export default class InlineComponent extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
if (info.name !== 'svelte:component' && info.name !== 'svelte:self') {
const name = info.name.split('.')[0]; // accommodate namespaces
component.warn_if_undefined(name, info, scope);

@ -13,6 +13,8 @@ export default class KeyBlock extends AbstractBlock {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression);

@ -2,4 +2,8 @@ import Tag from './shared/Tag';
export default class RawMustacheTag extends Tag {
type: 'RawMustacheTag';
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.not_static_content();
}
}

@ -60,5 +60,8 @@ export default class Slot extends Element {
}
component.slots.set(this.slot_name, this);
this.cannot_use_innerhtml();
this.not_static_content();
}
}

@ -15,6 +15,7 @@ export default class Node {
next?: INode;
can_use_innerhtml: boolean;
is_static_content: boolean;
var: string;
attributes: Attribute[];
@ -33,6 +34,9 @@ export default class Node {
value: parent
}
});
this.can_use_innerhtml = true;
this.is_static_content = true;
}
cannot_use_innerhtml() {
@ -42,6 +46,11 @@ export default class Node {
}
}
not_static_content() {
this.is_static_content = false;
if (this.parent) this.parent.not_static_content();
}
find_nearest(selector: RegExp) {
if (selector.test(this.type)) return this;
if (this.parent) return this.parent.find_nearest(selector);

@ -8,6 +8,9 @@ export default class Tag extends Node {
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
component.tags.push(this);
this.cannot_use_innerhtml();
this.expression = new Expression(component, this, scope, info.expression);
this.should_cache = (
@ -15,4 +18,12 @@ export default class Tag extends Node {
(this.expression.dependencies.size && scope.names.has(info.expression.name))
);
}
is_dependencies_static() {
return this.expression.contextual_dependencies.size === 0 && this.expression.dynamic_dependencies().length === 0;
}
check_if_content_dynamic() {
if (!this.is_dependencies_static()) {
this.not_static_content();
}
}
}

@ -133,9 +133,6 @@ export default class AwaitBlockWrapper extends Wrapper {
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
block.add_dependencies(this.node.expression.dependencies);
let is_dynamic = false;

@ -79,8 +79,6 @@ export default class EachBlockWrapper extends Wrapper {
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
const { dependencies } = node.expression;
block.add_dependencies(dependencies);

@ -36,9 +36,6 @@ export class BaseAttributeWrapper {
this.parent = parent;
if (node.dependencies.size > 0) {
parent.cannot_use_innerhtml();
parent.not_static_content();
block.add_dependencies(node.dependencies);
}
}

@ -262,24 +262,6 @@ export default class ElementWrapper extends Wrapper {
}
});
if (this.parent) {
if (node.actions.length > 0 ||
node.animation ||
node.bindings.length > 0 ||
node.classes.length > 0 ||
node.intro || node.outro ||
node.handlers.length > 0 ||
node.styles.length > 0 ||
this.node.name === 'option' ||
node.tag_expr.dynamic_dependencies().length ||
node.is_dynamic_element ||
renderer.options.dev
) {
this.parent.cannot_use_innerhtml(); // need to use add_location
this.parent.not_static_content();
}
}
this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling);
}
@ -417,6 +399,7 @@ export default class ElementWrapper extends Wrapper {
render_element(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
const { renderer } = this;
const hydratable = renderer.options.hydratable;
if (this.node.name === 'noscript') return;
@ -430,13 +413,15 @@ export default class ElementWrapper extends Wrapper {
b`${node} = ${render_statement};`
);
if (renderer.options.hydratable) {
const { can_use_textcontent, can_optimise_to_html_string } = this.node;
if (hydratable) {
if (parent_nodes) {
block.chunks.claim.push(b`
${node} = ${this.get_claim_statement(block, parent_nodes)};
${node} = ${this.get_claim_statement(block, parent_nodes, can_optimise_to_html_string)};
`);
if (!this.void && this.node.children.length > 0) {
if (!can_optimise_to_html_string && !this.void && this.node.children.length > 0) {
block.chunks.claim.push(b`
var ${nodes} = ${children};
`);
@ -474,15 +459,19 @@ export default class ElementWrapper extends Wrapper {
// insert static children with textContent or innerHTML
// skip textcontent for <template>. append nodes to TemplateElement.content instead
const can_use_textcontent = this.can_use_textcontent();
const is_template = this.node.name === 'template';
const is_template_with_text_content = is_template && can_use_textcontent;
if (!is_template_with_text_content && !this.node.namespace && (this.can_use_innerhtml || can_use_textcontent) && this.fragment.nodes.length > 0) {
if (can_optimise_to_html_string) {
if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') {
block.chunks.create.push(
b`${node}.textContent = ${string_literal((this.fragment.nodes[0] as TextWrapper).data)};`
);
let text: Node = string_literal((this.fragment.nodes[0] as TextWrapper).data);
if (hydratable) {
const variable = block.get_unique_name('textContent');
block.add_variable(variable, text);
text = variable;
}
block.chunks.create.push(b`${node}.textContent = ${text};`);
if (hydratable) {
block.chunks.claim.push(b`if (@get_svelte_dataset(${node}) !== "${this.node.hash()}") ${node}.textContent = ${text};`);
}
} else {
const state = {
quasi: {
@ -491,25 +480,33 @@ export default class ElementWrapper extends Wrapper {
}
};
const literal = {
let literal: Node = {
type: 'TemplateLiteral',
expressions: [],
quasis: []
};
const can_use_raw_text = !this.can_use_innerhtml && can_use_textcontent;
const can_use_raw_text = !this.node.can_use_innerhtml && can_use_textcontent;
to_html((this.fragment.nodes as unknown as Array<ElementWrapper | TextWrapper>), block, literal, state, can_use_raw_text);
literal.quasis.push(state.quasi);
literal.quasis.push(state.quasi as any);
block.chunks.create.push(
b`${node}.${this.can_use_innerhtml ? 'innerHTML' : 'textContent'} = ${literal};`
);
if (hydratable) {
const variable = block.get_unique_name('textContent');
block.add_variable(variable, literal);
literal = variable;
}
const property = this.node.can_use_innerhtml ? 'innerHTML' : 'textContent';
block.chunks.create.push(b`${node}.${property} = ${literal};`);
if (hydratable) {
block.chunks.claim.push(b`if (@get_svelte_dataset(${node}) !== "${this.node.hash()}") ${node}.${property} = ${literal};`);
}
}
} else {
this.fragment.nodes.forEach((child: Wrapper) => {
child.render(
block,
is_template ? x`${node}.content` : node,
this.node.name === 'template' ? x`${node}.content` : node,
nodes
);
});
@ -537,7 +534,7 @@ export default class ElementWrapper extends Wrapper {
this.add_styles(block);
this.add_manual_style_scoping(block);
if (nodes && this.renderer.options.hydratable && !this.void) {
if (nodes && hydratable && !this.void && !can_optimise_to_html_string) {
block.chunks.claim.push(
b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);`
);
@ -553,10 +550,6 @@ export default class ElementWrapper extends Wrapper {
block.renderer.dirty(this.node.tag_expr.dynamic_dependencies());
}
can_use_textcontent() {
return this.is_static_content && this.fragment.nodes.every(node => node.node.type === 'Text' || node.node.type === 'MustacheTag');
}
get_render_statement(block: Block) {
const { name, namespace, tag_expr } = this.node;
const reference = tag_expr.manipulate(block);
@ -577,7 +570,7 @@ export default class ElementWrapper extends Wrapper {
return x`@element(${reference})`;
}
get_claim_statement(block: Block, nodes: Identifier) {
get_claim_statement(block: Block, nodes: Identifier, can_optimise_to_html_string: boolean) {
const attributes = this.attributes
.filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name)
.map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`);
@ -595,6 +588,10 @@ export default class ElementWrapper extends Wrapper {
reference = x`(${this.node.tag_expr.manipulate(block)} || 'null').toUpperCase()`;
}
if (can_optimise_to_html_string) {
attributes.push(p`["data-svelte"]: true`);
}
if (this.node.namespace === namespaces.svg) {
return x`@claim_svg_element(${nodes}, ${reference}, { ${attributes} })`;
} else {

@ -20,8 +20,6 @@ export default class HeadWrapper extends Wrapper {
) {
super(renderer, block, parent, node);
this.can_use_innerhtml = false;
this.fragment = new FragmentWrapper(
renderer,
block,

@ -105,9 +105,6 @@ export default class IfBlockWrapper extends Wrapper {
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
this.branches = [];
const blocks: Block[] = [];

@ -44,9 +44,6 @@ export default class InlineComponentWrapper extends Wrapper {
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
if (this.node.expression) {
block.add_dependencies(this.node.expression.dependencies);
}

@ -24,9 +24,6 @@ export default class KeyBlockWrapper extends Wrapper {
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
this.dependencies = node.expression.dynamic_dependencies();
if (this.dependencies.length) {

@ -20,8 +20,6 @@ export default class RawMustacheTagWrapper extends Tag {
node: MustacheTag | RawMustacheTag
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
}
render(block: Block, parent_node: Identifier, _parent_nodes: Identifier) {

@ -30,8 +30,6 @@ export default class SlotWrapper extends Wrapper {
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
if (this.node.children.length) {
this.fallback = block.child({

@ -12,18 +12,9 @@ export default class Tag extends Wrapper {
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: MustacheTag | RawMustacheTag) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
if (!this.is_dependencies_static()) {
this.not_static_content();
}
block.add_dependencies(node.expression.dependencies);
}
is_dependencies_static() {
return this.node.expression.contextual_dependencies.size === 0 && this.node.expression.dynamic_dependencies().length === 0;
}
rename_this_method(
block: Block,
update: ((value: Node) => (Node | Node[]))

@ -13,8 +13,6 @@ export default class Wrapper {
next: Wrapper | null;
var: Identifier;
can_use_innerhtml: boolean;
is_static_content: boolean;
constructor(
renderer: Renderer,
@ -35,22 +33,9 @@ export default class Wrapper {
}
});
this.can_use_innerhtml = !renderer.options.hydratable;
this.is_static_content = !renderer.options.hydratable;
block.wrappers.push(this);
}
cannot_use_innerhtml() {
this.can_use_innerhtml = false;
if (this.parent) this.parent.cannot_use_innerhtml();
}
not_static_content() {
this.is_static_content = false;
if (this.parent) this.parent.not_static_content();
}
get_or_create_anchor(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
// TODO use this in EachBlock and IfBlock — tricky because
// children need to be created first

@ -47,6 +47,7 @@ const handlers: Record<string, Handler> = {
export interface RenderOptions extends CompileOptions{
locate: (c: number) => { line: number; column: number };
head_id?: string;
has_added_svelte_hash?: boolean;
}
export default class Renderer {

@ -160,6 +160,17 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
}
});
if (options.hydratable) {
if (options.head_id) {
renderer.add_string(` data-svelte="${options.head_id}"`);
}
if (node.can_optimise_to_html_string && !options.has_added_svelte_hash) {
renderer.add_string(` data-svelte="${node.hash()}"`);
options = { ...options, has_added_svelte_hash: true };
}
}
renderer.add_string('>');
if (node_contents !== undefined) {

@ -39,6 +39,7 @@ export default function remove_whitespace_children(children: INode[], next?: INo
continue;
}
child.data = data;
nodes.unshift(child);
link(last_child, last_child = child);
} else {

@ -1,4 +1,5 @@
export function string_literal(data: string) {
import { Literal } from 'estree';
export function string_literal(data: string): Literal {
return {
type: 'Literal',
value: data

@ -348,6 +348,10 @@ export function xlink_attr(node, attribute, value) {
node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
}
export function get_svelte_dataset(node: HTMLElement) {
return node.dataset.svelte;
}
export function get_binding_group_value(group, __value, checked) {
const value = new Set();
for (let i = 0; i < group.length; i += 1) {

@ -140,11 +140,12 @@ function cleanChildren(node) {
}
}
export function normalizeHtml(window, html, preserveComments = false) {
export function normalizeHtml(window, html, { removeDataSvelte = false, preserveComments = false }: { removeDataSvelte?: boolean, preserveComments?: boolean} = {}) {
try {
const node = window.document.createElement('div');
node.innerHTML = html
.replace(/(<!--.*?-->)/g, preserveComments ? '$1' : '')
.replace(/(data-svelte="[^"]+")/g, removeDataSvelte ? '': '$1')
.replace(/>[\s\r\n]+</g, '><')
.trim();
cleanChildren(node);
@ -154,22 +155,22 @@ export function normalizeHtml(window, html, preserveComments = false) {
}
}
export function setupHtmlEqual() {
export function setupHtmlEqual(options?: { removeDataSvelte?: boolean }) {
const window = env();
// eslint-disable-next-line no-import-assign
assert.htmlEqual = (actual, expected, message) => {
assert.deepEqual(
normalizeHtml(window, actual),
normalizeHtml(window, expected),
normalizeHtml(window, actual, options),
normalizeHtml(window, expected, options),
message
);
};
// eslint-disable-next-line no-import-assign
assert.htmlEqualWithComments = (actual, expected, message) => {
assert.deepEqual(
normalizeHtml(window, actual, true),
normalizeHtml(window, expected, true),
normalizeHtml(window, actual, { ...options, preserveComments: true }),
normalizeHtml(window, expected, { ...options, preserveComments: true }),
message
);
};

@ -1 +1 @@
<h1>Hello world!</h1>
<h1 data-svelte="svelte-1vv3a6r">Hello world!</h1>

@ -1 +1 @@
<h1>Hello world!</h1>
<h1 data-svelte="svelte-1vv3a6r">Hello world!</h1>

@ -0,0 +1,8 @@
<div data-svelte="xxx">hello</div>
<div data-svelte="xxx"><div>bye</div></div>
<div data-svelte="xxx">
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
<div data-svelte="xxx">hello</div>
<div data-svelte="xxx"><div data-svelte="yyy">bye</div></div>
<div data-svelte="xxx">
<div data-svelte="yyy">aaa</div>
<div data-svelte="zzz">bbb</div>
</div>

@ -0,0 +1,8 @@
<div>hello</div>
<div><div>bye</div></div>
<div>
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
<div>hello</div>
<div><div>bye</div></div>
<div>
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
<div>hello</div>
<div><div>bye</div></div>
<div>
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
<div>hello</div>
<div><div>bye</div></div>
<div>
<div>aaa</div>
<div>bbb</div>
</div>

@ -1,3 +1,3 @@
<div>
<p>nested</p>
<p data-svelte="svelte-1x3hbnh">nested</p>
</div>

@ -1,3 +1,3 @@
<div>
<p>nested</p>
<p data-svelte="svelte-1x3hbnh">nested</p>
</div>

@ -1 +1 @@
<p>nested</p>
<p data-svelte="svelte-1x3hbnh">nested</p>

@ -1 +1 @@
<p>nested</p>
<p data-svelte="svelte-1x3hbnh">nested</p>

@ -1,3 +1,3 @@
<div>
<div data-svelte="svelte-1aqf5aj">
<p>nested</p>
</div>

@ -1,3 +1,3 @@
<div>
<div data-svelte="svelte-1aqf5aj">
<p>nested</p>
</div>

@ -45,7 +45,7 @@ describe('runtime', () => {
return module._compile(code, filename);
};
return setupHtmlEqual();
return setupHtmlEqual({ removeDataSvelte: true });
});
after(() => process.removeListener('unhandledRejection', unhandledRejection_handler));
@ -159,10 +159,12 @@ describe('runtime', () => {
// ssr into target
compileOptions.generate = 'ssr';
cleanRequireCache();
if (config.before_test) config.before_test();
const SsrSvelteComponent = require(`./samples/${dir}/main.svelte`).default;
const { html } = SsrSvelteComponent.render(config.props);
target.innerHTML = html;
delete compileOptions.generate;
if (config.after_test) config.after_test();
} else {
target.innerHTML = '';
}

@ -5,7 +5,9 @@ export default {
<h1>tag is h1.</h1>
`,
props: {
logs
pushLogs(log) {
logs.push(log);
}
},
after_test() {
logs = [];

@ -1,12 +1,12 @@
<script>
export let logs = [];
export let pushLogs;
export let tag = "h1";
export let opt = "opt1";
function foo(node, {tag, opt}) {
logs.push(`create: ${tag},${opt}`);
pushLogs(`create: ${tag},${opt}`);
return {
update: ({tag, opt}) => logs.push(`update: ${tag},${opt}`),
destroy: () => logs.push('destroy'),
update: ({tag, opt}) => pushLogs(`update: ${tag},${opt}`),
destroy: () => pushLogs('destroy'),
};
}
</script>

@ -1,3 +1,3 @@
<div>Just a dummy page.</div>
<div data-svelte="svelte-156ixv6">Just a dummy page.</div>

@ -11,11 +11,9 @@
<div>A
B
<span>C
D
</span>
D</span>
E
F
</div>
F</div>
<div><pre> A
B

Loading…
Cancel
Save