feat(compiler): hoist static trees
This commit is contained in:
parent
2e2b6924da
commit
095f5edf8d
@ -5,7 +5,8 @@ import {
|
|||||||
ObjectExpression,
|
ObjectExpression,
|
||||||
CompilerOptions,
|
CompilerOptions,
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
NodeTypes
|
NodeTypes,
|
||||||
|
CallExpression
|
||||||
} from '../../src'
|
} from '../../src'
|
||||||
import { transformOn } from '../../src/transforms/vOn'
|
import { transformOn } from '../../src/transforms/vOn'
|
||||||
import { transformElement } from '../../src/transforms/transformElement'
|
import { transformElement } from '../../src/transforms/transformElement'
|
||||||
@ -29,7 +30,8 @@ function parseWithVOn(
|
|||||||
describe('compiler: transform v-on', () => {
|
describe('compiler: transform v-on', () => {
|
||||||
test('basic', () => {
|
test('basic', () => {
|
||||||
const node = parseWithVOn(`<div v-on:click="onClick"/>`)
|
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({
|
expect(props.properties[0]).toMatchObject({
|
||||||
key: {
|
key: {
|
||||||
content: `onClick`,
|
content: `onClick`,
|
||||||
@ -64,7 +66,8 @@ describe('compiler: transform v-on', () => {
|
|||||||
|
|
||||||
test('dynamic arg', () => {
|
test('dynamic arg', () => {
|
||||||
const node = parseWithVOn(`<div v-on:[event]="handler"/>`)
|
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({
|
expect(props.properties[0]).toMatchObject({
|
||||||
key: {
|
key: {
|
||||||
type: NodeTypes.COMPOUND_EXPRESSION,
|
type: NodeTypes.COMPOUND_EXPRESSION,
|
||||||
@ -82,7 +85,8 @@ describe('compiler: transform v-on', () => {
|
|||||||
const node = parseWithVOn(`<div v-on:[event]="handler"/>`, {
|
const node = parseWithVOn(`<div v-on:[event]="handler"/>`, {
|
||||||
prefixIdentifiers: true
|
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({
|
expect(props.properties[0]).toMatchObject({
|
||||||
key: {
|
key: {
|
||||||
type: NodeTypes.COMPOUND_EXPRESSION,
|
type: NodeTypes.COMPOUND_EXPRESSION,
|
||||||
@ -100,7 +104,8 @@ describe('compiler: transform v-on', () => {
|
|||||||
const node = parseWithVOn(`<div v-on:[event(foo)]="handler"/>`, {
|
const node = parseWithVOn(`<div v-on:[event(foo)]="handler"/>`, {
|
||||||
prefixIdentifiers: true
|
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({
|
expect(props.properties[0]).toMatchObject({
|
||||||
key: {
|
key: {
|
||||||
type: NodeTypes.COMPOUND_EXPRESSION,
|
type: NodeTypes.COMPOUND_EXPRESSION,
|
||||||
@ -123,7 +128,8 @@ describe('compiler: transform v-on', () => {
|
|||||||
|
|
||||||
test('should wrap as function if expression is inline statement', () => {
|
test('should wrap as function if expression is inline statement', () => {
|
||||||
const node = parseWithVOn(`<div @click="i++"/>`)
|
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({
|
expect(props.properties[0]).toMatchObject({
|
||||||
key: { content: `onClick` },
|
key: { content: `onClick` },
|
||||||
value: {
|
value: {
|
||||||
@ -137,7 +143,8 @@ describe('compiler: transform v-on', () => {
|
|||||||
const node = parseWithVOn(`<div @click="foo($event)"/>`, {
|
const node = parseWithVOn(`<div @click="foo($event)"/>`, {
|
||||||
prefixIdentifiers: true
|
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({
|
expect(props.properties[0]).toMatchObject({
|
||||||
key: { content: `onClick` },
|
key: { content: `onClick` },
|
||||||
value: {
|
value: {
|
||||||
@ -157,7 +164,8 @@ describe('compiler: transform v-on', () => {
|
|||||||
|
|
||||||
test('should NOT wrap as function if expression is already function expression', () => {
|
test('should NOT wrap as function if expression is already function expression', () => {
|
||||||
const node = parseWithVOn(`<div @click="$event => foo($event)"/>`)
|
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({
|
expect(props.properties[0]).toMatchObject({
|
||||||
key: { content: `onClick` },
|
key: { content: `onClick` },
|
||||||
value: {
|
value: {
|
||||||
@ -171,7 +179,8 @@ describe('compiler: transform v-on', () => {
|
|||||||
const node = parseWithVOn(`<div @click="e => foo(e)"/>`, {
|
const node = parseWithVOn(`<div @click="e => foo(e)"/>`, {
|
||||||
prefixIdentifiers: true
|
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({
|
expect(props.properties[0]).toMatchObject({
|
||||||
key: { content: `onClick` },
|
key: { content: `onClick` },
|
||||||
value: {
|
value: {
|
||||||
|
@ -6,7 +6,8 @@ import {
|
|||||||
ElementNode,
|
ElementNode,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
ForNode
|
ForNode,
|
||||||
|
CallExpression
|
||||||
} from '../../src'
|
} from '../../src'
|
||||||
import { transformElement } from '../../src/transforms/transformElement'
|
import { transformElement } from '../../src/transforms/transformElement'
|
||||||
import { transformOn } from '../../src/transforms/vOn'
|
import { transformOn } from '../../src/transforms/vOn'
|
||||||
@ -44,7 +45,7 @@ function parseWithSlots(template: string, options: CompilerOptions = {}) {
|
|||||||
root: ast,
|
root: ast,
|
||||||
slots:
|
slots:
|
||||||
ast.children[0].type === NodeTypes.ELEMENT
|
ast.children[0].type === NodeTypes.ELEMENT
|
||||||
? ast.children[0].codegenNode!.arguments[2]
|
? (ast.children[0].codegenNode as CallExpression).arguments[2]
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ export interface ElementNode extends Node {
|
|||||||
isSelfClosing: boolean
|
isSelfClosing: boolean
|
||||||
props: Array<AttributeNode | DirectiveNode>
|
props: Array<AttributeNode | DirectiveNode>
|
||||||
children: TemplateChildNode[]
|
children: TemplateChildNode[]
|
||||||
codegenNode: CallExpression | undefined
|
codegenNode: CallExpression | SimpleExpressionNode | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TextNode extends Node {
|
export interface TextNode extends Node {
|
||||||
|
@ -185,8 +185,15 @@ export function generate(
|
|||||||
if (prefixIdentifiers) {
|
if (prefixIdentifiers) {
|
||||||
push(`const { ${ast.imports.join(', ')} } = Vue\n`)
|
push(`const { ${ast.imports.join(', ')} } = Vue\n`)
|
||||||
} else {
|
} else {
|
||||||
|
// "with" mode.
|
||||||
// save Vue in a separate variable to avoid collision
|
// save Vue in a separate variable to avoid collision
|
||||||
push(`const _Vue = Vue\n`)
|
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)
|
genHoists(ast.hoists, context)
|
||||||
|
@ -16,6 +16,7 @@ import { isString, isArray } from '@vue/shared'
|
|||||||
import { CompilerError, defaultOnError } from './errors'
|
import { CompilerError, defaultOnError } from './errors'
|
||||||
import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
|
import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
|
||||||
import { isVSlot, createBlockExpression, isSlotOutlet } from './utils'
|
import { isVSlot, createBlockExpression, isSlotOutlet } from './utils'
|
||||||
|
import { hoistStaticTrees } from './transforms/hoistStatic'
|
||||||
|
|
||||||
// There are two types of transforms:
|
// There are two types of transforms:
|
||||||
//
|
//
|
||||||
@ -50,6 +51,7 @@ export interface TransformOptions {
|
|||||||
nodeTransforms?: NodeTransform[]
|
nodeTransforms?: NodeTransform[]
|
||||||
directiveTransforms?: { [name: string]: DirectiveTransform }
|
directiveTransforms?: { [name: string]: DirectiveTransform }
|
||||||
prefixIdentifiers?: boolean
|
prefixIdentifiers?: boolean
|
||||||
|
hoistStaticTrees?: boolean
|
||||||
onError?: (error: CompilerError) => void
|
onError?: (error: CompilerError) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +83,7 @@ function createTransformContext(
|
|||||||
root: RootNode,
|
root: RootNode,
|
||||||
{
|
{
|
||||||
prefixIdentifiers = false,
|
prefixIdentifiers = false,
|
||||||
|
hoistStaticTrees = false,
|
||||||
nodeTransforms = [],
|
nodeTransforms = [],
|
||||||
directiveTransforms = {},
|
directiveTransforms = {},
|
||||||
onError = defaultOnError
|
onError = defaultOnError
|
||||||
@ -99,6 +102,7 @@ function createTransformContext(
|
|||||||
vOnce: 0
|
vOnce: 0
|
||||||
},
|
},
|
||||||
prefixIdentifiers,
|
prefixIdentifiers,
|
||||||
|
hoistStaticTrees,
|
||||||
nodeTransforms,
|
nodeTransforms,
|
||||||
directiveTransforms,
|
directiveTransforms,
|
||||||
onError,
|
onError,
|
||||||
@ -200,6 +204,9 @@ function createTransformContext(
|
|||||||
export function transform(root: RootNode, options: TransformOptions) {
|
export function transform(root: RootNode, options: TransformOptions) {
|
||||||
const context = createTransformContext(root, options)
|
const context = createTransformContext(root, options)
|
||||||
traverseNode(root, context)
|
traverseNode(root, context)
|
||||||
|
if (options.hoistStaticTrees) {
|
||||||
|
hoistStaticTrees(root, context)
|
||||||
|
}
|
||||||
finalizeRoot(root, context)
|
finalizeRoot(root, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +218,8 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
|
|||||||
if (
|
if (
|
||||||
child.type === NodeTypes.ELEMENT &&
|
child.type === NodeTypes.ELEMENT &&
|
||||||
!isSlotOutlet(child) &&
|
!isSlotOutlet(child) &&
|
||||||
child.codegenNode
|
child.codegenNode &&
|
||||||
|
child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION
|
||||||
) {
|
) {
|
||||||
// turn root element into a block
|
// turn root element into a block
|
||||||
root.codegenNode = createBlockExpression(
|
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,
|
createFunctionExpression,
|
||||||
ElementTypes,
|
ElementTypes,
|
||||||
createObjectExpression,
|
createObjectExpression,
|
||||||
createObjectProperty
|
createObjectProperty,
|
||||||
|
CallExpression
|
||||||
} from '../ast'
|
} from '../ast'
|
||||||
import { createCompilerError, ErrorCodes } from '../errors'
|
import { createCompilerError, ErrorCodes } from '../errors'
|
||||||
import {
|
import {
|
||||||
@ -117,7 +118,7 @@ export const transformFor = createStructuralDirectiveTransform(
|
|||||||
: null
|
: null
|
||||||
if (slotOutlet) {
|
if (slotOutlet) {
|
||||||
// <slot v-for="..."> or <template v-for="..."><slot/></template>
|
// <slot v-for="..."> or <template v-for="..."><slot/></template>
|
||||||
childBlock = slotOutlet.codegenNode!
|
childBlock = slotOutlet.codegenNode as CallExpression
|
||||||
if (isTemplate && keyProperty) {
|
if (isTemplate && keyProperty) {
|
||||||
// <template v-for="..." :key="..."><slot/></template>
|
// <template v-for="..." :key="..."><slot/></template>
|
||||||
// we need to inject the key to the renderSlot() call.
|
// 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
|
// Normal element v-for. Directly use the child's codegenNode
|
||||||
// arguments, but replace createVNode() with createBlock()
|
// arguments, but replace createVNode() with createBlock()
|
||||||
childBlock = createBlockExpression(
|
childBlock = createBlockExpression(
|
||||||
node.codegenNode!.arguments,
|
(node.codegenNode as CallExpression).arguments,
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -179,7 +179,7 @@ function createChildrenCodegenNode(
|
|||||||
}
|
}
|
||||||
return createCallExpression(helper(CREATE_BLOCK), blockArgs)
|
return createCallExpression(helper(CREATE_BLOCK), blockArgs)
|
||||||
} else {
|
} else {
|
||||||
const childCodegen = (child as ElementNode).codegenNode!
|
const childCodegen = (child as ElementNode).codegenNode as CallExpression
|
||||||
let vnodeCall = childCodegen
|
let vnodeCall = childCodegen
|
||||||
// Element with custom directives. Locate the actual createVNode() call.
|
// Element with custom directives. Locate the actual createVNode() call.
|
||||||
if (vnodeCall.callee.includes(APPLY_DIRECTIVES)) {
|
if (vnodeCall.callee.includes(APPLY_DIRECTIVES)) {
|
||||||
|
@ -175,7 +175,7 @@ export const isTemplateNode = (
|
|||||||
|
|
||||||
export const isSlotOutlet = (
|
export const isSlotOutlet = (
|
||||||
node: RootNode | TemplateChildNode
|
node: RootNode | TemplateChildNode
|
||||||
): node is ElementNode & { tagType: ElementTypes.SLOT } =>
|
): node is ElementNode & { tagType: ElementTypes.ELEMENT } =>
|
||||||
node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT
|
node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT
|
||||||
|
|
||||||
export function injectProp(
|
export function injectProp(
|
||||||
|
@ -3,7 +3,8 @@ import {
|
|||||||
transform,
|
transform,
|
||||||
CompilerOptions,
|
CompilerOptions,
|
||||||
ElementNode,
|
ElementNode,
|
||||||
NodeTypes
|
NodeTypes,
|
||||||
|
CallExpression
|
||||||
} from '@vue/compiler-core'
|
} from '@vue/compiler-core'
|
||||||
import { transformBind } from '../../../compiler-core/src/transforms/vBind'
|
import { transformBind } from '../../../compiler-core/src/transforms/vBind'
|
||||||
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
|
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
|
||||||
@ -59,7 +60,7 @@ describe('compiler: style transform', () => {
|
|||||||
bind: transformBind
|
bind: transformBind
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(node.codegenNode!.arguments[1]).toMatchObject({
|
expect((node.codegenNode as CallExpression).arguments[1]).toMatchObject({
|
||||||
type: NodeTypes.JS_OBJECT_EXPRESSION,
|
type: NodeTypes.JS_OBJECT_EXPRESSION,
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
|
@ -7,7 +7,10 @@ function compileToFunction(
|
|||||||
template: string,
|
template: string,
|
||||||
options?: CompilerOptions
|
options?: CompilerOptions
|
||||||
): RenderFunction {
|
): RenderFunction {
|
||||||
const { code } = compile(template, options)
|
const { code } = compile(template, {
|
||||||
|
hoistStaticTrees: true,
|
||||||
|
...options
|
||||||
|
})
|
||||||
return new Function(code)() as RenderFunction
|
return new Function(code)() as RenderFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user