From 095f5edf8dad8a44adaba6d1d1556168e8eec108 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 3 Oct 2019 23:30:25 -0400 Subject: [PATCH] feat(compiler): hoist static trees --- .../__tests__/transforms/vOn.spec.ts | 27 +++-- .../__tests__/transforms/vSlot.spec.ts | 5 +- packages/compiler-core/src/ast.ts | 2 +- packages/compiler-core/src/codegen.ts | 7 ++ packages/compiler-core/src/transform.ts | 10 +- .../src/transforms/hoistStatic.ts | 98 +++++++++++++++++++ packages/compiler-core/src/transforms/vFor.ts | 7 +- packages/compiler-core/src/transforms/vIf.ts | 2 +- packages/compiler-core/src/utils.ts | 2 +- .../transforms/transformStyle.spec.ts | 5 +- packages/vue/src/index.ts | 5 +- 11 files changed, 149 insertions(+), 21 deletions(-) create mode 100644 packages/compiler-core/src/transforms/hoistStatic.ts diff --git a/packages/compiler-core/__tests__/transforms/vOn.spec.ts b/packages/compiler-core/__tests__/transforms/vOn.spec.ts index 9a7ba450..114fd529 100644 --- a/packages/compiler-core/__tests__/transforms/vOn.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vOn.spec.ts @@ -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(`
`) - 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(`
`) - 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(`
`, { 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(`
`, { 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(`
`) - 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(`
`, { 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(`
`) - 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(`
`, { 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: { diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts index 1b556424..9e7213e9 100644 --- a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts @@ -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 } } diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 4eff8ff8..23b8f965 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -90,7 +90,7 @@ export interface ElementNode extends Node { isSelfClosing: boolean props: Array children: TemplateChildNode[] - codegenNode: CallExpression | undefined + codegenNode: CallExpression | SimpleExpressionNode | undefined } export interface TextNode extends Node { diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 189e180b..dfde1305 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -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) diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 588d3e49..01ee0ff2 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -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 index 00000000..99594367 --- /dev/null +++ b/packages/compiler-core/src/transforms/hoistStatic.ts @@ -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()) +} + +function walk( + children: TemplateChildNode[], + context: TransformContext, + knownStaticNodes: Set +) { + 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 +): 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 + } +} diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts index 30fbf24d..6deaa306 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -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) { // or - childBlock = slotOutlet.codegenNode! + childBlock = slotOutlet.codegenNode as CallExpression if (isTemplate && keyProperty) { // // 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 ) } diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts index 47eda281..b78d395c 100644 --- a/packages/compiler-core/src/transforms/vIf.ts +++ b/packages/compiler-core/src/transforms/vIf.ts @@ -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)) { diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index ab19a7d0..87d81e85 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -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( diff --git a/packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts b/packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts index b9410c7a..87aa7f95 100644 --- a/packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts @@ -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: [ { diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index e2baaaf6..98c4b598 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -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 }