]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler): optimize text by merging adjacent nodes
authorEvan You <yyx990803@gmail.com>
Mon, 30 Sep 2019 18:51:55 +0000 (14:51 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 30 Sep 2019 18:52:10 +0000 (14:52 -0400)
packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap
packages/compiler-core/__tests__/codegen.spec.ts
packages/compiler-core/__tests__/transform.spec.ts
packages/compiler-core/__tests__/transforms/optimizeText.spec.ts [new file with mode: 0644]
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/optimizeInterpolations.ts [deleted file]
packages/compiler-core/src/transforms/optimizeText.ts [new file with mode: 0644]
packages/compiler-core/src/transforms/vIf.ts

index 527f8b1093d7eab071acabd0d8acb42693441d59..e388868723eaf6f08f745a6e2c8e4083b0d49c16 100644 (file)
@@ -44,7 +44,7 @@ exports[`compiler: codegen compound expression 1`] = `
 "
 return function render() {
   with (this) {
-    return _toString(_ctx.foo)
+    return _ctx.foo + _toString(bar)
   }
 }"
 `;
index 19a1f0b18e39ade5035c61abf43bba6c1835813a..93de38fbc115bf7c380dc08b2859be7670d33db4 100644 (file)
@@ -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()
   })
 
index 8ebf221f93628a4b56191b485ca18095afedf290..b23340344ab3d42918447943d9ba99cdd205a894 100644 (file)
@@ -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(`<div :id="foo"/><div :id="bar"/>`)
     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 (file)
index 0000000..64ed2da
--- /dev/null
@@ -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(`<div/>{{ foo }} bar {{ baz }}<div/>`)
+    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(
+      `<div/>{{ foo }} bar {{ baz }}<div/>{{ foo }} bar {{ baz }}<div/>`
+    )
+    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` }]
+          }
+        }
+      ]
+    })
+  })
+})
index 97a92f5b0f1f623425fb7832aede57e3b7510db5..30a3d6c07f48b072e52087260e7ce0ddfed25490 100644 (file)
@@ -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[]
index 0fb5a5954685443da8874ecbc50c18ff5a736d61..ee0d5d2b6bb220ec8d60ff5fbd6a95dddb14df43 100644 (file)
@@ -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)
     }
   }
 }
index 111f97e1ddbcf1ac06067d3a3003e5a90c3a169c..e6f51c6ca007eeeb54b31fff6c20b2df296088e3 100644 (file)
@@ -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,
index 6a4f893a0ecbd579798f45122a7d86ba528489ea..263c77dd7590057cd809e08d3f52e413c88ed618 100644 (file)
@@ -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<TransformOptions> {
   statements: Set<string>
   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 (file)
index 13cfc4b..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-// TODO merge adjacent text nodes and expressions into a single expression
-// e.g. <div>abc {{ d }} {{ e }}</div> 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 (file)
index 0000000..09e637e
--- /dev/null
@@ -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. <div>abc {{ d }} {{ e }}</div> 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
+            }
+          }
+        }
+      }
+    }
+  }
+}
index 96d340f332bc0dabdf9f35a6f60ba3454957a45c..966cc41b40d796ce453d22e1122547fba413e6ff 100644 (file)
@@ -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) {