From e5e40e1e38109a017efed2e308e986193a12daca Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 30 Sep 2019 14:51:55 -0400 Subject: [PATCH] feat(compiler): optimize text by merging adjacent nodes --- .../__snapshots__/codegen.spec.ts.snap | 2 +- .../compiler-core/__tests__/codegen.spec.ts | 18 ++- .../compiler-core/__tests__/transform.spec.ts | 49 ++++---- .../__tests__/transforms/optimizeText.spec.ts | 112 ++++++++++++++++++ packages/compiler-core/src/ast.ts | 3 +- packages/compiler-core/src/codegen.ts | 3 +- packages/compiler-core/src/index.ts | 2 + packages/compiler-core/src/transform.ts | 36 ++++-- .../src/transforms/optimizeInterpolations.ts | 2 - .../src/transforms/optimizeText.ts | 48 ++++++++ packages/compiler-core/src/transforms/vIf.ts | 2 +- 11 files changed, 233 insertions(+), 44 deletions(-) create mode 100644 packages/compiler-core/__tests__/transforms/optimizeText.spec.ts delete mode 100644 packages/compiler-core/src/transforms/optimizeInterpolations.ts create mode 100644 packages/compiler-core/src/transforms/optimizeText.ts diff --git a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap index 527f8b10..e3888687 100644 --- a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap @@ -44,7 +44,7 @@ exports[`compiler: codegen compound expression 1`] = ` " return function render() { with (this) { - return _toString(_ctx.foo) + return _ctx.foo + _toString(bar) } }" `; diff --git a/packages/compiler-core/__tests__/codegen.spec.ts b/packages/compiler-core/__tests__/codegen.spec.ts index 19a1f0b1..93de38fb 100644 --- a/packages/compiler-core/__tests__/codegen.spec.ts +++ b/packages/compiler-core/__tests__/codegen.spec.ts @@ -226,17 +226,23 @@ describe('compiler: codegen', () => { const { code } = generate( createRoot({ children: [ - createInterpolation( - createCompoundExpression( - [`_ctx.`, createSimpleExpression(`foo`, false, mockLoc)], - mockLoc - ), + createCompoundExpression( + [ + `_ctx.`, + createSimpleExpression(`foo`, false, mockLoc), + ` + `, + { + type: NodeTypes.INTERPOLATION, + loc: mockLoc, + content: createSimpleExpression(`bar`, false, mockLoc) + } + ], mockLoc ) ] }) ) - expect(code).toMatch(`return _${TO_STRING}(_ctx.foo)`) + expect(code).toMatch(`return _ctx.foo + _${TO_STRING}(bar)`) expect(code).toMatchSnapshot() }) diff --git a/packages/compiler-core/__tests__/transform.spec.ts b/packages/compiler-core/__tests__/transform.spec.ts index 8ebf221f..b2334034 100644 --- a/packages/compiler-core/__tests__/transform.spec.ts +++ b/packages/compiler-core/__tests__/transform.spec.ts @@ -25,22 +25,29 @@ describe('compiler: transform', () => { }) const div = ast.children[0] as ElementNode - expect(calls.length).toBe(3) + expect(calls.length).toBe(4) expect(calls[0]).toMatchObject([ + ast, + { + parent: null, + currentNode: ast + } + ]) + expect(calls[1]).toMatchObject([ div, { parent: ast, currentNode: div } ]) - expect(calls[1]).toMatchObject([ + expect(calls[2]).toMatchObject([ div.children[0], { parent: div, currentNode: div.children[0] } ]) - expect(calls[2]).toMatchObject([ + expect(calls[3]).toMatchObject([ div.children[1], { parent: div, @@ -76,11 +83,11 @@ describe('compiler: transform', () => { expect(ast.children.length).toBe(2) const newElement = ast.children[0] as ElementNode expect(newElement.tag).toBe('p') - expect(spy).toHaveBeenCalledTimes(3) + expect(spy).toHaveBeenCalledTimes(4) // should traverse the children of replaced node - expect(spy.mock.calls[1][0]).toBe(newElement.children[0]) + expect(spy.mock.calls[2][0]).toBe(newElement.children[0]) // should traverse the node after the replaced node - expect(spy.mock.calls[2][0]).toBe(ast.children[1]) + expect(spy.mock.calls[3][0]).toBe(ast.children[1]) }) test('context.removeNode', () => { @@ -103,10 +110,10 @@ describe('compiler: transform', () => { expect(ast.children[1]).toBe(c2) // should not traverse children of remove node - expect(spy).toHaveBeenCalledTimes(3) + expect(spy).toHaveBeenCalledTimes(4) // should traverse nodes around removed - expect(spy.mock.calls[0][0]).toBe(c1) - expect(spy.mock.calls[2][0]).toBe(c2) + expect(spy.mock.calls[1][0]).toBe(c1) + expect(spy.mock.calls[3][0]).toBe(c2) }) test('context.removeNode (prev sibling)', () => { @@ -118,7 +125,7 @@ describe('compiler: transform', () => { if (node.type === NodeTypes.ELEMENT && node.tag === 'div') { context.removeNode() // remove previous sibling - context.removeNode(context.parent.children[0]) + context.removeNode(context.parent!.children[0]) } } const spy = jest.fn(plugin) @@ -129,11 +136,11 @@ describe('compiler: transform', () => { expect(ast.children.length).toBe(1) expect(ast.children[0]).toBe(c2) - expect(spy).toHaveBeenCalledTimes(3) + expect(spy).toHaveBeenCalledTimes(4) // should still traverse first span before removal - expect(spy.mock.calls[0][0]).toBe(c1) + expect(spy.mock.calls[1][0]).toBe(c1) // should still traverse last span - expect(spy.mock.calls[2][0]).toBe(c2) + expect(spy.mock.calls[3][0]).toBe(c2) }) test('context.removeNode (next sibling)', () => { @@ -145,7 +152,7 @@ describe('compiler: transform', () => { if (node.type === NodeTypes.ELEMENT && node.tag === 'div') { context.removeNode() // remove next sibling - context.removeNode(context.parent.children[1]) + context.removeNode(context.parent!.children[1]) } } const spy = jest.fn(plugin) @@ -156,20 +163,22 @@ describe('compiler: transform', () => { expect(ast.children.length).toBe(1) expect(ast.children[0]).toBe(c1) - expect(spy).toHaveBeenCalledTimes(2) + expect(spy).toHaveBeenCalledTimes(3) // should still traverse first span before removal - expect(spy.mock.calls[0][0]).toBe(c1) + expect(spy.mock.calls[1][0]).toBe(c1) // should not traverse last span - expect(spy.mock.calls[1][0]).toBe(d1) + expect(spy.mock.calls[2][0]).toBe(d1) }) test('context.hoist', () => { const ast = parse(`
`) const hoisted: ExpressionNode[] = [] const mock: NodeTransform = (node, context) => { - const dir = (node as ElementNode).props[0] as DirectiveNode - hoisted.push(dir.exp!) - dir.exp = context.hoist(dir.exp!) + if (node.type === NodeTypes.ELEMENT) { + const dir = node.props[0] as DirectiveNode + hoisted.push(dir.exp!) + dir.exp = context.hoist(dir.exp!) + } } transform(ast, { nodeTransforms: [mock] diff --git a/packages/compiler-core/__tests__/transforms/optimizeText.spec.ts b/packages/compiler-core/__tests__/transforms/optimizeText.spec.ts new file mode 100644 index 00000000..64ed2da0 --- /dev/null +++ b/packages/compiler-core/__tests__/transforms/optimizeText.spec.ts @@ -0,0 +1,112 @@ +import { CompilerOptions, parse, transform, NodeTypes } from '../../src' +import { optimizeText } from '../../src/transforms/optimizeText' +import { transformExpression } from '../../src/transforms/transformExpression' + +function transformWithTextOpt(template: string, options: CompilerOptions = {}) { + const ast = parse(template) + transform(ast, { + nodeTransforms: [ + ...(options.prefixIdentifiers ? [transformExpression] : []), + optimizeText + ], + ...options + }) + return ast +} + +describe('compiler: optimize interpolation', () => { + test('no consecutive text', () => { + const root = transformWithTextOpt(`{{ foo }}`) + expect(root.children[0]).toMatchObject({ + type: NodeTypes.INTERPOLATION, + content: { + content: `foo` + } + }) + }) + + test('consecutive text', () => { + const root = transformWithTextOpt(`{{ foo }} bar {{ baz }}`) + expect(root.children.length).toBe(1) + expect(root.children[0]).toMatchObject({ + type: NodeTypes.COMPOUND_EXPRESSION, + children: [ + { type: NodeTypes.INTERPOLATION, content: { content: `foo` } }, + ` + `, + { type: NodeTypes.TEXT, content: ` bar ` }, + ` + `, + { type: NodeTypes.INTERPOLATION, content: { content: `baz` } } + ] + }) + }) + + test('consecutive text between elements', () => { + const root = transformWithTextOpt(`
{{ foo }} bar {{ baz }}
`) + expect(root.children.length).toBe(3) + expect(root.children[0].type).toBe(NodeTypes.ELEMENT) + expect(root.children[1]).toMatchObject({ + type: NodeTypes.COMPOUND_EXPRESSION, + children: [ + { type: NodeTypes.INTERPOLATION, content: { content: `foo` } }, + ` + `, + { type: NodeTypes.TEXT, content: ` bar ` }, + ` + `, + { type: NodeTypes.INTERPOLATION, content: { content: `baz` } } + ] + }) + expect(root.children[2].type).toBe(NodeTypes.ELEMENT) + }) + + test('consecutive text mixed with elements', () => { + const root = transformWithTextOpt( + `
{{ foo }} bar {{ baz }}
{{ foo }} bar {{ baz }}
` + ) + expect(root.children.length).toBe(5) + expect(root.children[0].type).toBe(NodeTypes.ELEMENT) + expect(root.children[1]).toMatchObject({ + type: NodeTypes.COMPOUND_EXPRESSION, + children: [ + { type: NodeTypes.INTERPOLATION, content: { content: `foo` } }, + ` + `, + { type: NodeTypes.TEXT, content: ` bar ` }, + ` + `, + { type: NodeTypes.INTERPOLATION, content: { content: `baz` } } + ] + }) + expect(root.children[2].type).toBe(NodeTypes.ELEMENT) + expect(root.children[3]).toMatchObject({ + type: NodeTypes.COMPOUND_EXPRESSION, + children: [ + { type: NodeTypes.INTERPOLATION, content: { content: `foo` } }, + ` + `, + { type: NodeTypes.TEXT, content: ` bar ` }, + ` + `, + { type: NodeTypes.INTERPOLATION, content: { content: `baz` } } + ] + }) + expect(root.children[4].type).toBe(NodeTypes.ELEMENT) + }) + + test('with prefixIdentifiers: true', () => { + const root = transformWithTextOpt(`{{ foo }} bar {{ baz + qux }}`, { + prefixIdentifiers: true + }) + expect(root.children.length).toBe(1) + expect(root.children[0]).toMatchObject({ + type: NodeTypes.COMPOUND_EXPRESSION, + children: [ + { type: NodeTypes.INTERPOLATION, content: { content: `_ctx.foo` } }, + ` + `, + { type: NodeTypes.TEXT, content: ` bar ` }, + ` + `, + { + type: NodeTypes.INTERPOLATION, + content: { + type: NodeTypes.COMPOUND_EXPRESSION, + children: [{ content: `_ctx.baz` }, ` + `, { content: `_ctx.qux` }] + } + } + ] + }) + }) +}) diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 97a92f5b..30a3d6c0 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -64,6 +64,7 @@ export type ExpressionNode = SimpleExpressionNode | CompoundExpressionNode export type ChildNode = | ElementNode | InterpolationNode + | CompoundExpressionNode | TextNode | CommentNode | IfNode @@ -130,7 +131,7 @@ export interface InterpolationNode extends Node { // always dynamic export interface CompoundExpressionNode extends Node { type: NodeTypes.COMPOUND_EXPRESSION - children: (SimpleExpressionNode | string)[] + children: (SimpleExpressionNode | InterpolationNode | TextNode | string)[] // an expression parsed as the params of a function will track // the identifiers declared inside the function body. identifiers?: string[] diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 0fb5a595..ee0d5d2b 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -283,6 +283,7 @@ function genChildren( (allowSingle || type === NodeTypes.TEXT || type === NodeTypes.INTERPOLATION || + type === NodeTypes.COMPOUND_EXPRESSION || (type === NodeTypes.ELEMENT && (child as ElementNode).tagType === ElementTypes.SLOT)) ) { @@ -423,7 +424,7 @@ function genCompoundExpression( if (isString(child)) { context.push(child) } else { - genExpression(child, context) + genNode(child, context) } } } diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 111f97e1..e6f51c6c 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -13,6 +13,7 @@ import { transformOn } from './transforms/vOn' import { transformBind } from './transforms/vBind' import { defaultOnError, createCompilerError, ErrorCodes } from './errors' import { trackSlotScopes } from './transforms/vSlot' +import { optimizeText } from './transforms/optimizeText' export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions @@ -45,6 +46,7 @@ export function baseCompile( transformIf, transformFor, ...(prefixIdentifiers ? [transformExpression, trackSlotScopes] : []), + optimizeText, transformStyle, transformSlotOutlet, transformElement, diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 6a4f893a..263c77dd 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -20,7 +20,7 @@ import { TO_STRING, COMMENT, CREATE_VNODE } from './runtimeConstants' // Transforms that operate directly on a ChildNode. NodeTransforms may mutate, // replace or remove the node being processed. export type NodeTransform = ( - node: ChildNode, + node: RootNode | ChildNode, context: TransformContext ) => void | (() => void) | (() => void)[] @@ -56,9 +56,9 @@ export interface TransformContext extends Required { statements: Set hoists: JSChildNode[] identifiers: { [name: string]: number | undefined } - parent: ParentNode + parent: ParentNode | null childIndex: number - currentNode: ChildNode | null + currentNode: RootNode | ChildNode | null helper(name: string): string replaceNode(node: ChildNode): void removeNode(node?: ChildNode): void @@ -87,22 +87,30 @@ function createTransformContext( nodeTransforms, directiveTransforms, onError, - parent: root, + parent: null, + currentNode: root, childIndex: 0, - currentNode: null, helper(name) { context.imports.add(name) return prefixIdentifiers ? name : `_${name}` }, replaceNode(node) { /* istanbul ignore if */ - if (__DEV__ && !context.currentNode) { - throw new Error(`node being replaced is already removed.`) + if (__DEV__) { + if (!context.currentNode) { + throw new Error(`Node being replaced is already removed.`) + } + if (!context.parent) { + throw new Error(`Cannot replace root node.`) + } } - context.parent.children[context.childIndex] = context.currentNode = node + context.parent!.children[context.childIndex] = context.currentNode = node }, removeNode(node) { - const list = context.parent.children + if (__DEV__ && !context.parent) { + throw new Error(`Cannot remove root node.`) + } + const list = context.parent!.children const removalIndex = node ? list.indexOf(node as any) : context.currentNode @@ -123,7 +131,7 @@ function createTransformContext( context.onNodeRemoved() } } - context.parent.children.splice(removalIndex, 1) + context.parent!.children.splice(removalIndex, 1) }, onNodeRemoved: () => {}, addIdentifiers(exp) { @@ -172,7 +180,7 @@ function createTransformContext( export function transform(root: RootNode, options: TransformOptions) { const context = createTransformContext(root, options) - traverseChildren(root, context) + traverseNode(root, context) root.imports = [...context.imports] root.statements = [...context.statements] root.hoists = context.hoists @@ -197,7 +205,10 @@ export function traverseChildren( } } -export function traverseNode(node: ChildNode, context: TransformContext) { +export function traverseNode( + node: RootNode | ChildNode, + context: TransformContext +) { // apply transform plugins const { nodeTransforms } = context const exitFns = [] @@ -240,6 +251,7 @@ export function traverseNode(node: ChildNode, context: TransformContext) { break case NodeTypes.FOR: case NodeTypes.ELEMENT: + case NodeTypes.ROOT: traverseChildren(node, context) break } diff --git a/packages/compiler-core/src/transforms/optimizeInterpolations.ts b/packages/compiler-core/src/transforms/optimizeInterpolations.ts deleted file mode 100644 index 13cfc4b2..00000000 --- a/packages/compiler-core/src/transforms/optimizeInterpolations.ts +++ /dev/null @@ -1,2 +0,0 @@ -// TODO merge adjacent text nodes and expressions into a single expression -// e.g.
abc {{ d }} {{ e }}
should have a single expression node as child. diff --git a/packages/compiler-core/src/transforms/optimizeText.ts b/packages/compiler-core/src/transforms/optimizeText.ts new file mode 100644 index 00000000..09e637ee --- /dev/null +++ b/packages/compiler-core/src/transforms/optimizeText.ts @@ -0,0 +1,48 @@ +import { NodeTransform } from '../transform' +import { + NodeTypes, + ChildNode, + TextNode, + InterpolationNode, + CompoundExpressionNode +} from '../ast' + +const isText = (node: ChildNode): node is TextNode | InterpolationNode => + node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT + +// Merge adjacent text nodes and expressions into a single expression +// e.g.
abc {{ d }} {{ e }}
should have a single expression node as child. +export const optimizeText: NodeTransform = node => { + if (node.type === NodeTypes.ROOT || node.type === NodeTypes.ELEMENT) { + // perform the transform on node exit so that all expressions have already + // been processed. + return () => { + const children = node.children + let currentContainer: CompoundExpressionNode | undefined = undefined + for (let i = 0; i < children.length; i++) { + const child = children[i] + if (isText(child)) { + for (let j = i + 1; j < children.length; j++) { + const next = children[j] + if (isText(next)) { + if (!currentContainer) { + currentContainer = children[i] = { + type: NodeTypes.COMPOUND_EXPRESSION, + loc: child.loc, + children: [child] + } + } + // merge adjacent text node into current + currentContainer.children.push(` + `, next) + children.splice(j, 1) + j-- + } else { + currentContainer = undefined + break + } + } + } + } + } + } +} diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts index 96d340f3..966cc41b 100644 --- a/packages/compiler-core/src/transforms/vIf.ts +++ b/packages/compiler-core/src/transforms/vIf.ts @@ -29,7 +29,7 @@ export const transformIf = createStructuralDirectiveTransform( }) } else { // locate the adjacent v-if - const siblings = context.parent.children + const siblings = context.parent!.children const comments = [] let i = siblings.indexOf(node as any) while (i-- >= -1) {