feat(compiler): hoist static trees
This commit is contained in:
parent
2e2b6924da
commit
095f5edf8d
@ -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: {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
98
packages/compiler-core/src/transforms/hoistStatic.ts
Normal file
98
packages/compiler-core/src/transforms/hoistStatic.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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(
|
||||
|
@ -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: [
|
||||
{
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user