]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler): hoist static trees
authorEvan You <yyx990803@gmail.com>
Fri, 4 Oct 2019 03:30:25 +0000 (23:30 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 4 Oct 2019 03:30:25 +0000 (23:30 -0400)
packages/compiler-core/__tests__/transforms/vOn.spec.ts
packages/compiler-core/__tests__/transforms/vSlot.spec.ts
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/hoistStatic.ts [new file with mode: 0644]
packages/compiler-core/src/transforms/vFor.ts
packages/compiler-core/src/transforms/vIf.ts
packages/compiler-core/src/utils.ts
packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts
packages/vue/src/index.ts

index 9a7ba45000d962707d0df9212e7654b67a2cbbff..114fd529cfc85447efeac2102d3930881c9653ca 100644 (file)
@@ -5,7 +5,8 @@ import {
   ObjectExpression,
   CompilerOptions,
   ErrorCodes,
-  NodeTypes
+  NodeTypes,
+  CallExpression
 } from '../../src'
 import { transformOn } from '../../src/transforms/vOn'
 import { transformElement } from '../../src/transforms/transformElement'
@@ -29,7 +30,8 @@ function parseWithVOn(
 describe('compiler: transform v-on', () => {
   test('basic', () => {
     const node = parseWithVOn(`<div v-on:click="onClick"/>`)
-    const props = node.codegenNode!.arguments[1] as ObjectExpression
+    const props = (node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression
     expect(props.properties[0]).toMatchObject({
       key: {
         content: `onClick`,
@@ -64,7 +66,8 @@ describe('compiler: transform v-on', () => {
 
   test('dynamic arg', () => {
     const node = parseWithVOn(`<div v-on:[event]="handler"/>`)
-    const props = node.codegenNode!.arguments[1] as ObjectExpression
+    const props = (node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression
     expect(props.properties[0]).toMatchObject({
       key: {
         type: NodeTypes.COMPOUND_EXPRESSION,
@@ -82,7 +85,8 @@ describe('compiler: transform v-on', () => {
     const node = parseWithVOn(`<div v-on:[event]="handler"/>`, {
       prefixIdentifiers: true
     })
-    const props = node.codegenNode!.arguments[1] as ObjectExpression
+    const props = (node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression
     expect(props.properties[0]).toMatchObject({
       key: {
         type: NodeTypes.COMPOUND_EXPRESSION,
@@ -100,7 +104,8 @@ describe('compiler: transform v-on', () => {
     const node = parseWithVOn(`<div v-on:[event(foo)]="handler"/>`, {
       prefixIdentifiers: true
     })
-    const props = node.codegenNode!.arguments[1] as ObjectExpression
+    const props = (node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression
     expect(props.properties[0]).toMatchObject({
       key: {
         type: NodeTypes.COMPOUND_EXPRESSION,
@@ -123,7 +128,8 @@ describe('compiler: transform v-on', () => {
 
   test('should wrap as function if expression is inline statement', () => {
     const node = parseWithVOn(`<div @click="i++"/>`)
-    const props = node.codegenNode!.arguments[1] as ObjectExpression
+    const props = (node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression
     expect(props.properties[0]).toMatchObject({
       key: { content: `onClick` },
       value: {
@@ -137,7 +143,8 @@ describe('compiler: transform v-on', () => {
     const node = parseWithVOn(`<div @click="foo($event)"/>`, {
       prefixIdentifiers: true
     })
-    const props = node.codegenNode!.arguments[1] as ObjectExpression
+    const props = (node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression
     expect(props.properties[0]).toMatchObject({
       key: { content: `onClick` },
       value: {
@@ -157,7 +164,8 @@ describe('compiler: transform v-on', () => {
 
   test('should NOT wrap as function if expression is already function expression', () => {
     const node = parseWithVOn(`<div @click="$event => foo($event)"/>`)
-    const props = node.codegenNode!.arguments[1] as ObjectExpression
+    const props = (node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression
     expect(props.properties[0]).toMatchObject({
       key: { content: `onClick` },
       value: {
@@ -171,7 +179,8 @@ describe('compiler: transform v-on', () => {
     const node = parseWithVOn(`<div @click="e => foo(e)"/>`, {
       prefixIdentifiers: true
     })
-    const props = node.codegenNode!.arguments[1] as ObjectExpression
+    const props = (node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression
     expect(props.properties[0]).toMatchObject({
       key: { content: `onClick` },
       value: {
index 1b556424659394be9c8b200d04a0b26e34faa9ba..9e7213e9bb3c94e513612950601d8dfc9ff2e137 100644 (file)
@@ -6,7 +6,8 @@ import {
   ElementNode,
   NodeTypes,
   ErrorCodes,
-  ForNode
+  ForNode,
+  CallExpression
 } from '../../src'
 import { transformElement } from '../../src/transforms/transformElement'
 import { transformOn } from '../../src/transforms/vOn'
@@ -44,7 +45,7 @@ function parseWithSlots(template: string, options: CompilerOptions = {}) {
     root: ast,
     slots:
       ast.children[0].type === NodeTypes.ELEMENT
-        ? ast.children[0].codegenNode!.arguments[2]
+        ? (ast.children[0].codegenNode as CallExpression).arguments[2]
         : null
   }
 }
index 4eff8ff834399b58e710464a6b03beda5b6713ed..23b8f965021b6a4afe73b0040c7953a62a823b4e 100644 (file)
@@ -90,7 +90,7 @@ export interface ElementNode extends Node {
   isSelfClosing: boolean
   props: Array<AttributeNode | DirectiveNode>
   children: TemplateChildNode[]
-  codegenNode: CallExpression | undefined
+  codegenNode: CallExpression | SimpleExpressionNode | undefined
 }
 
 export interface TextNode extends Node {
index 189e180b5fed8da0735ce5a9312d13b84f27e62d..dfde1305e898f70f12727db8882927b1536bbb8a 100644 (file)
@@ -185,8 +185,15 @@ export function generate(
       if (prefixIdentifiers) {
         push(`const { ${ast.imports.join(', ')} } = Vue\n`)
       } else {
+        // "with" mode.
         // save Vue in a separate variable to avoid collision
         push(`const _Vue = Vue\n`)
+        // in "with" mode, helpers are declared inside the with block to avoid
+        // has check cost, but hosits are lifted out of the function - we need
+        // to provide the helper here.
+        if (ast.hoists.length) {
+          push(`const _${CREATE_VNODE} = Vue.createVNode\n`)
+        }
       }
     }
     genHoists(ast.hoists, context)
index 588d3e4985bc5c07dc10165dc974ad3a6283b1bc..01ee0ff2c66c7e4e21f0ba79f3d9326f14c5eb78 100644 (file)
@@ -16,6 +16,7 @@ import { isString, isArray } from '@vue/shared'
 import { CompilerError, defaultOnError } from './errors'
 import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
 import { isVSlot, createBlockExpression, isSlotOutlet } from './utils'
+import { hoistStaticTrees } from './transforms/hoistStatic'
 
 // There are two types of transforms:
 //
@@ -50,6 +51,7 @@ export interface TransformOptions {
   nodeTransforms?: NodeTransform[]
   directiveTransforms?: { [name: string]: DirectiveTransform }
   prefixIdentifiers?: boolean
+  hoistStaticTrees?: boolean
   onError?: (error: CompilerError) => void
 }
 
@@ -81,6 +83,7 @@ function createTransformContext(
   root: RootNode,
   {
     prefixIdentifiers = false,
+    hoistStaticTrees = false,
     nodeTransforms = [],
     directiveTransforms = {},
     onError = defaultOnError
@@ -99,6 +102,7 @@ function createTransformContext(
       vOnce: 0
     },
     prefixIdentifiers,
+    hoistStaticTrees,
     nodeTransforms,
     directiveTransforms,
     onError,
@@ -200,6 +204,9 @@ function createTransformContext(
 export function transform(root: RootNode, options: TransformOptions) {
   const context = createTransformContext(root, options)
   traverseNode(root, context)
+  if (options.hoistStaticTrees) {
+    hoistStaticTrees(root, context)
+  }
   finalizeRoot(root, context)
 }
 
@@ -211,7 +218,8 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
     if (
       child.type === NodeTypes.ELEMENT &&
       !isSlotOutlet(child) &&
-      child.codegenNode
+      child.codegenNode &&
+      child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION
     ) {
       // turn root element into a block
       root.codegenNode = createBlockExpression(
diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts
new file mode 100644 (file)
index 0000000..9959436
--- /dev/null
@@ -0,0 +1,98 @@
+import {
+  RootNode,
+  NodeTypes,
+  TemplateChildNode,
+  CallExpression,
+  ElementNode
+} from '../ast'
+import { TransformContext } from '../transform'
+import { CREATE_VNODE } from '../runtimeConstants'
+import { PropsExpression } from './transformElement'
+
+export function hoistStaticTrees(root: RootNode, context: TransformContext) {
+  walk(root.children, context, new Set<TemplateChildNode>())
+}
+
+function walk(
+  children: TemplateChildNode[],
+  context: TransformContext,
+  knownStaticNodes: Set<TemplateChildNode>
+) {
+  for (let i = 0; i < children.length; i++) {
+    const child = children[i]
+    if (child.type === NodeTypes.ELEMENT) {
+      if (isStaticNode(child, knownStaticNodes)) {
+        // whole tree is static
+        child.codegenNode = context.hoist(child.codegenNode!)
+        continue
+      } else if (!getPatchFlag(child)) {
+        // has dynamic children, but self props are static, hoist props instead
+        const props = (child.codegenNode as CallExpression).arguments[1] as
+          | PropsExpression
+          | `null`
+        if (props !== `null`) {
+          ;(child.codegenNode as CallExpression).arguments[1] = context.hoist(
+            props
+          )
+        }
+      }
+    }
+    if (child.type === NodeTypes.ELEMENT || child.type === NodeTypes.FOR) {
+      walk(child.children, context, knownStaticNodes)
+    } else if (child.type === NodeTypes.IF) {
+      for (let i = 0; i < child.branches.length; i++) {
+        walk(child.branches[i].children, context, knownStaticNodes)
+      }
+    }
+  }
+}
+
+function getPatchFlag(node: ElementNode): number | undefined {
+  const codegenNode = node.codegenNode as CallExpression
+  if (
+    // callee is createVNode (i.e. no runtime directives)
+    codegenNode.callee.includes(CREATE_VNODE)
+  ) {
+    const flag = codegenNode.arguments[3]
+    return flag ? parseInt(flag as string, 10) : undefined
+  }
+}
+
+function isStaticNode(
+  node: TemplateChildNode,
+  knownStaticNodes: Set<TemplateChildNode>
+): boolean {
+  switch (node.type) {
+    case NodeTypes.ELEMENT:
+      if (knownStaticNodes.has(node)) {
+        return true
+      }
+      const flag = getPatchFlag(node)
+      if (!flag) {
+        // element self is static. check its children.
+        for (let i = 0; i < node.children.length; i++) {
+          if (!isStaticNode(node.children[i], knownStaticNodes)) {
+            return false
+          }
+        }
+        knownStaticNodes.add(node)
+        return true
+      } else {
+        return false
+      }
+    case NodeTypes.TEXT:
+    case NodeTypes.COMMENT:
+      return true
+    case NodeTypes.IF:
+    case NodeTypes.FOR:
+    case NodeTypes.INTERPOLATION:
+    case NodeTypes.COMPOUND_EXPRESSION:
+      return false
+    default:
+      if (__DEV__) {
+        const exhaustiveCheck: never = node
+        exhaustiveCheck
+      }
+      return false
+  }
+}
index 30fbf24d6d0f88dfe13601af9d3bce5559427c9c..6deaa30667acfb34a56ad793ee7a67524a52ced5 100644 (file)
@@ -13,7 +13,8 @@ import {
   createFunctionExpression,
   ElementTypes,
   createObjectExpression,
-  createObjectProperty
+  createObjectProperty,
+  CallExpression
 } from '../ast'
 import { createCompilerError, ErrorCodes } from '../errors'
 import {
@@ -117,7 +118,7 @@ export const transformFor = createStructuralDirectiveTransform(
             : null
           if (slotOutlet) {
             // <slot v-for="..."> or <template v-for="..."><slot/></template>
-            childBlock = slotOutlet.codegenNode!
+            childBlock = slotOutlet.codegenNode as CallExpression
             if (isTemplate && keyProperty) {
               // <template v-for="..." :key="..."><slot/></template>
               // we need to inject the key to the renderSlot() call.
@@ -147,7 +148,7 @@ export const transformFor = createStructuralDirectiveTransform(
             // Normal element v-for. Directly use the child's codegenNode
             // arguments, but replace createVNode() with createBlock()
             childBlock = createBlockExpression(
-              node.codegenNode!.arguments,
+              (node.codegenNode as CallExpression).arguments,
               context
             )
           }
index 47eda2812311428d954130859867b942bb4e1d2c..b78d395c3988b6e2341cb0b5783aea08bfc24068 100644 (file)
@@ -179,7 +179,7 @@ function createChildrenCodegenNode(
     }
     return createCallExpression(helper(CREATE_BLOCK), blockArgs)
   } else {
-    const childCodegen = (child as ElementNode).codegenNode!
+    const childCodegen = (child as ElementNode).codegenNode as CallExpression
     let vnodeCall = childCodegen
     // Element with custom directives. Locate the actual createVNode() call.
     if (vnodeCall.callee.includes(APPLY_DIRECTIVES)) {
index ab19a7d0428a7f596c189aa40fe77d156146a576..87d81e851d249ade4b716dfe3d3c835c279af4e1 100644 (file)
@@ -175,7 +175,7 @@ export const isTemplateNode = (
 
 export const isSlotOutlet = (
   node: RootNode | TemplateChildNode
-): node is ElementNode & { tagType: ElementTypes.SLOT } =>
+): node is ElementNode & { tagType: ElementTypes.ELEMENT } =>
   node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT
 
 export function injectProp(
index b9410c7a542fe1e51cb9564c103ee80d7947c52e..87aa7f958f7665d2098b4d261389188db4bd0ec7 100644 (file)
@@ -3,7 +3,8 @@ import {
   transform,
   CompilerOptions,
   ElementNode,
-  NodeTypes
+  NodeTypes,
+  CallExpression
 } from '@vue/compiler-core'
 import { transformBind } from '../../../compiler-core/src/transforms/vBind'
 import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
@@ -59,7 +60,7 @@ describe('compiler: style transform', () => {
         bind: transformBind
       }
     })
-    expect(node.codegenNode!.arguments[1]).toMatchObject({
+    expect((node.codegenNode as CallExpression).arguments[1]).toMatchObject({
       type: NodeTypes.JS_OBJECT_EXPRESSION,
       properties: [
         {
index e2baaaf6f58666aafc8d551c1c73814fd8286e6d..98c4b59815f0a5dc44623e7052bc9c9e903cfb1b 100644 (file)
@@ -7,7 +7,10 @@ function compileToFunction(
   template: string,
   options?: CompilerOptions
 ): RenderFunction {
-  const { code } = compile(template, options)
+  const { code } = compile(template, {
+    hoistStaticTrees: true,
+    ...options
+  })
   return new Function(code)() as RenderFunction
 }