feat(compiler): hoist static trees

This commit is contained in:
Evan You 2019-10-03 23:30:25 -04:00
parent 2e2b6924da
commit 095f5edf8d
11 changed files with 149 additions and 21 deletions

View 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: {

View 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
}
}

View 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 {

View 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)

View 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(

View File

@ -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
}
}

View 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
)
}

View 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)) {

View 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(

View 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: [
{

View 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
}