refactor(compiler): use symbols for runtime helpers

This commit is contained in:
Evan You
2019-10-05 17:18:25 -04:00
parent 7c4eea6048
commit bfecf2cdce
26 changed files with 420 additions and 231 deletions

View File

@@ -1,5 +1,6 @@
import { isString } from '@vue/shared'
import { ForParseResult } from './transforms/vFor'
import { CREATE_VNODE, RuntimeHelper } from './runtimeHelpers'
// Vue template is a platform-agnostic superset of HTML (syntax only).
// More namespaces like SVG and MathML are declared by platform specific
@@ -76,8 +77,9 @@ export type TemplateChildNode =
export interface RootNode extends Node {
type: NodeTypes.ROOT
children: TemplateChildNode[]
imports: string[]
statements: string[]
helpers: RuntimeHelper[]
components: string[]
directives: string[]
hoists: JSChildNode[]
codegenNode: TemplateChildNode | JSChildNode | undefined
}
@@ -93,6 +95,29 @@ export interface ElementNode extends Node {
codegenNode: CallExpression | SimpleExpressionNode | undefined
}
export interface PlainElementNode extends ElementNode {
tagType: ElementTypes.ELEMENT
codegenNode: VNodeCodegenNode | VNodeWithDirectiveCodegenNode
}
export interface ComponentNode extends ElementNode {
tagType: ElementTypes.COMPONENT
codegenNode: VNodeCodegenNode | VNodeWithDirectiveCodegenNode
}
export interface SlotOutletNode extends ElementNode {
tagType: ElementTypes.SLOT
codegenNode: SlotOutletCodegenNode
}
export interface VNodeCodegenNode extends CallExpression {
callee: typeof CREATE_VNODE
}
export interface VNodeWithDirectiveCodegenNode extends CallExpression {}
export interface SlotOutletCodegenNode extends CallExpression {}
export interface TextNode extends Node {
type: NodeTypes.TEXT
content: string
@@ -137,7 +162,12 @@ export interface InterpolationNode extends Node {
// always dynamic
export interface CompoundExpressionNode extends Node {
type: NodeTypes.COMPOUND_EXPRESSION
children: (SimpleExpressionNode | InterpolationNode | TextNode | string)[]
children: (
| SimpleExpressionNode
| InterpolationNode
| TextNode
| string
| RuntimeHelper)[]
// an expression parsed as the params of a function will track
// the identifiers declared inside the function body.
identifiers?: string[]
@@ -146,9 +176,11 @@ export interface CompoundExpressionNode extends Node {
export interface IfNode extends Node {
type: NodeTypes.IF
branches: IfBranchNode[]
codegenNode: SequenceExpression
codegenNode: IfCodegenNode
}
export interface IfCodegenNode extends SequenceExpression {}
export interface IfBranchNode extends Node {
type: NodeTypes.IF_BRANCH
condition: ExpressionNode | undefined // else
@@ -162,9 +194,11 @@ export interface ForNode extends Node {
keyAlias: ExpressionNode | undefined
objectIndexAlias: ExpressionNode | undefined
children: TemplateChildNode[]
codegenNode: SequenceExpression
codegenNode: ForCodegenNode
}
export interface ForCodegenNode extends SequenceExpression {}
// We also include a number of JavaScript AST nodes for code generation.
// The AST is an intentionally minimal subset just to meet the exact needs of
// Vue render function generation.
@@ -179,8 +213,13 @@ export type JSChildNode =
export interface CallExpression extends Node {
type: NodeTypes.JS_CALL_EXPRESSION
callee: string
arguments: (string | JSChildNode | TemplateChildNode | TemplateChildNode[])[]
callee: string | RuntimeHelper
arguments: (
| string
| RuntimeHelper
| JSChildNode
| TemplateChildNode
| TemplateChildNode[])[]
}
export interface ObjectExpression extends Node {

View File

@@ -23,10 +23,19 @@ import {
advancePositionWithMutation,
assert,
isSimpleIdentifier,
loadDep
loadDep,
toValidAssetId
} from './utils'
import { isString, isArray } from '@vue/shared'
import { TO_STRING, CREATE_VNODE, COMMENT } from './runtimeConstants'
import { isString, isArray, isSymbol } from '@vue/shared'
import {
TO_STRING,
CREATE_VNODE,
COMMENT,
helperNameMap,
RESOLVE_COMPONENT,
RESOLVE_DIRECTIVE,
RuntimeHelper
} from './runtimeHelpers'
type CodegenNode = TemplateChildNode | JSChildNode
@@ -65,7 +74,7 @@ export interface CodegenContext extends Required<CodegenOptions> {
offset: number
indentLevel: number
map?: SourceMapGenerator
helper(name: string): string
helper(key: RuntimeHelper): string
push(code: string, node?: CodegenNode, openOnly?: boolean): void
resetMapping(loc: SourceLocation): void
indent(): void
@@ -77,7 +86,7 @@ function createCodegenContext(
ast: RootNode,
{
mode = 'function',
prefixIdentifiers = false,
prefixIdentifiers = mode === 'module',
sourceMap = false,
filename = `template.vue.html`
}: CodegenOptions
@@ -100,7 +109,8 @@ function createCodegenContext(
? undefined
: new (loadDep('source-map')).SourceMapGenerator(),
helper(name) {
helper(key) {
const name = helperNameMap[key]
return prefixIdentifiers ? name : `_${name}`
},
push(code, node, openOnly) {
@@ -172,8 +182,16 @@ export function generate(
options: CodegenOptions = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
const { mode, push, prefixIdentifiers, indent, deindent, newline } = context
const hasImports = ast.imports.length
const {
mode,
push,
helper,
prefixIdentifiers,
indent,
deindent,
newline
} = context
const hasHelpers = ast.helpers.length > 0
const useWithBlock = !prefixIdentifiers && mode !== 'module'
// preambles
@@ -182,9 +200,9 @@ export function generate(
// In prefix mode, we place the const declaration at top so it's done
// only once; But if we not prefixing, we place the declaration inside the
// with block so it doesn't incur the `in` check cost for every helper access.
if (hasImports) {
if (hasHelpers) {
if (prefixIdentifiers) {
push(`const { ${ast.imports.join(', ')} } = Vue\n`)
push(`const { ${ast.helpers.map(helper).join(', ')} } = Vue\n`)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
@@ -193,7 +211,7 @@ export function generate(
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
if (ast.hoists.length) {
push(`const _${CREATE_VNODE} = Vue.createVNode\n`)
push(`const _${helperNameMap[CREATE_VNODE]} = Vue.createVNode\n`)
}
}
}
@@ -202,8 +220,8 @@ export function generate(
push(`return `)
} else {
// generate import statements for helpers
if (hasImports) {
push(`import { ${ast.imports.join(', ')} } from "vue"\n`)
if (hasHelpers) {
push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
}
genHoists(ast.hoists, context)
context.newline()
@@ -219,8 +237,12 @@ export function generate(
indent()
// function mode const declarations should be inside with block
// also they should be renamed to avoid collision with user properties
if (hasImports) {
push(`const { ${ast.imports.map(n => `${n}: _${n}`).join(', ')} } = _Vue`)
if (hasHelpers) {
push(
`const { ${ast.helpers
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')} } = _Vue`
)
newline()
newline()
}
@@ -230,11 +252,13 @@ export function generate(
}
// generate asset resolution statements
if (ast.statements.length) {
ast.statements.forEach(s => {
push(s)
newline()
})
if (ast.components.length) {
genAssets(ast.components, 'component', context)
}
if (ast.directives.length) {
genAssets(ast.directives, 'directive', context)
}
if (ast.components.length || ast.directives.length) {
newline()
}
@@ -260,6 +284,23 @@ export function generate(
}
}
function genAssets(
assets: string[],
type: 'component' | 'directive',
context: CodegenContext
) {
const resolver = context.helper(
type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE
)
for (let i = 0; i < assets.length; i++) {
const id = assets[i]
context.push(
`const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)})`
)
context.newline()
}
}
function genHoists(hoists: JSChildNode[], context: CodegenContext) {
if (!hoists.length) {
return
@@ -297,7 +338,7 @@ function genNodeListAsArray(
}
function genNodeList(
nodes: (string | CodegenNode | TemplateChildNode[])[],
nodes: (string | RuntimeHelper | CodegenNode | TemplateChildNode[])[],
context: CodegenContext,
multilines: boolean = false
) {
@@ -322,11 +363,18 @@ function genNodeList(
}
}
function genNode(node: CodegenNode | string, context: CodegenContext) {
function genNode(
node: CodegenNode | RuntimeHelper | string,
context: CodegenContext
) {
if (isString(node)) {
context.push(node)
return
}
if (isSymbol(node)) {
context.push(context.helper(node))
return
}
switch (node.type) {
case NodeTypes.ELEMENT:
case NodeTypes.IF:
@@ -450,7 +498,10 @@ function genComment(node: CommentNode, context: CodegenContext) {
// JavaScript
function genCallExpression(node: CallExpression, context: CodegenContext) {
context.push(node.callee + `(`, node, true)
const callee = isString(node.callee)
? node.callee
: context.helper(node.callee)
context.push(callee + `(`, node, true)
genNodeList(node.arguments, context)
context.push(`)`)
}

View File

@@ -64,7 +64,10 @@ export function baseCompile(
}
})
return generate(ast, options)
return generate(ast, {
...options,
prefixIdentifiers
})
}
// Also expose lower level APIs & types

View File

@@ -1,21 +0,0 @@
// Name mapping constants for runtime helpers that need to be imported in
// generated code. Make sure these are correctly exported in the runtime!
export const FRAGMENT = `Fragment`
export const PORTAL = `Portal`
export const COMMENT = `Comment`
export const TEXT = `Text`
export const SUSPENSE = `Suspense`
export const EMPTY = `Empty`
export const OPEN_BLOCK = `openBlock`
export const CREATE_BLOCK = `createBlock`
export const CREATE_VNODE = `createVNode`
export const RESOLVE_COMPONENT = `resolveComponent`
export const RESOLVE_DIRECTIVE = `resolveDirective`
export const APPLY_DIRECTIVES = `applyDirectives`
export const RENDER_LIST = `renderList`
export const RENDER_SLOT = `renderSlot`
export const CREATE_SLOTS = `createSlots`
export const TO_STRING = `toString`
export const MERGE_PROPS = `mergeProps`
export const TO_HANDLERS = `toHandlers`
export const CAMELIZE = `camelize`

View File

@@ -0,0 +1,64 @@
export const FRAGMENT = Symbol()
export const PORTAL = Symbol()
export const COMMENT = Symbol()
export const TEXT = Symbol()
export const SUSPENSE = Symbol()
export const EMPTY = Symbol()
export const OPEN_BLOCK = Symbol()
export const CREATE_BLOCK = Symbol()
export const CREATE_VNODE = Symbol()
export const RESOLVE_COMPONENT = Symbol()
export const RESOLVE_DIRECTIVE = Symbol()
export const APPLY_DIRECTIVES = Symbol()
export const RENDER_LIST = Symbol()
export const RENDER_SLOT = Symbol()
export const CREATE_SLOTS = Symbol()
export const TO_STRING = Symbol()
export const MERGE_PROPS = Symbol()
export const TO_HANDLERS = Symbol()
export const CAMELIZE = Symbol()
export type RuntimeHelper =
| typeof FRAGMENT
| typeof PORTAL
| typeof COMMENT
| typeof TEXT
| typeof SUSPENSE
| typeof EMPTY
| typeof OPEN_BLOCK
| typeof CREATE_BLOCK
| typeof CREATE_VNODE
| typeof RESOLVE_COMPONENT
| typeof RESOLVE_DIRECTIVE
| typeof APPLY_DIRECTIVES
| typeof RENDER_LIST
| typeof RENDER_SLOT
| typeof CREATE_SLOTS
| typeof TO_STRING
| typeof MERGE_PROPS
| typeof TO_HANDLERS
| typeof CAMELIZE
// Name mapping for runtime helpers that need to be imported from 'vue' in
// generated code. Make sure these are correctly exported in the runtime!
export const helperNameMap = {
[FRAGMENT]: `Fragment`,
[PORTAL]: `Portal`,
[COMMENT]: `Comment`,
[TEXT]: `Text`,
[SUSPENSE]: `Suspense`,
[EMPTY]: `Empty`,
[OPEN_BLOCK]: `openBlock`,
[CREATE_BLOCK]: `createBlock`,
[CREATE_VNODE]: `createVNode`,
[RESOLVE_COMPONENT]: `resolveComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`,
[APPLY_DIRECTIVES]: `applyDirectives`,
[RENDER_LIST]: `renderList`,
[RENDER_SLOT]: `renderSlot`,
[CREATE_SLOTS]: `createSlots`,
[TO_STRING]: `toString`,
[MERGE_PROPS]: `mergeProps`,
[TO_HANDLERS]: `toHandlers`,
[CAMELIZE]: `camelize`
}

View File

@@ -14,7 +14,14 @@ import {
} from './ast'
import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors'
import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
import {
TO_STRING,
COMMENT,
CREATE_VNODE,
FRAGMENT,
RuntimeHelper,
helperNameMap
} from './runtimeHelpers'
import { isVSlot, createBlockExpression, isSlotOutlet } from './utils'
import { hoistStatic } from './transforms/hoistStatic'
@@ -57,8 +64,9 @@ export interface TransformOptions {
export interface TransformContext extends Required<TransformOptions> {
root: RootNode
imports: Set<string>
statements: Set<string>
helpers: Set<RuntimeHelper>
components: Set<string>
directives: Set<string>
hoists: JSChildNode[]
identifiers: { [name: string]: number | undefined }
scopes: {
@@ -70,7 +78,8 @@ export interface TransformContext extends Required<TransformOptions> {
parent: ParentNode | null
childIndex: number
currentNode: RootNode | TemplateChildNode | null
helper(name: string): string
helper<T extends RuntimeHelper>(name: T): T
helperString(name: RuntimeHelper): string
replaceNode(node: TemplateChildNode): void
removeNode(node?: TemplateChildNode): void
onNodeRemoved: () => void
@@ -91,8 +100,9 @@ function createTransformContext(
): TransformContext {
const context: TransformContext = {
root,
imports: new Set(),
statements: new Set(),
helpers: new Set(),
components: new Set(),
directives: new Set(),
hoists: [],
identifiers: {},
scopes: {
@@ -110,8 +120,14 @@ function createTransformContext(
currentNode: root,
childIndex: 0,
helper(name) {
context.imports.add(name)
return prefixIdentifiers ? name : `_${name}`
context.helpers.add(name)
return name
},
helperString(name) {
return (
(context.prefixIdentifiers ? `` : `_`) +
helperNameMap[context.helper(name)]
)
},
replaceNode(node) {
/* istanbul ignore if */
@@ -242,8 +258,9 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
}
// finalize meta information
root.imports = [...context.imports]
root.statements = [...context.statements]
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.hoists = context.hoists
}

View File

@@ -7,7 +7,7 @@ import {
ElementTypes
} from '../ast'
import { TransformContext } from '../transform'
import { APPLY_DIRECTIVES } from '../runtimeConstants'
import { APPLY_DIRECTIVES } from '../runtimeHelpers'
import { PropsExpression } from './transformElement'
import { PatchFlags } from '@vue/shared'
@@ -41,7 +41,7 @@ function walk(
flag === PatchFlags.TEXT
) {
let codegenNode = child.codegenNode as CallExpression
if (codegenNode.callee.includes(APPLY_DIRECTIVES)) {
if (codegenNode.callee === APPLY_DIRECTIVES) {
codegenNode = codegenNode.arguments[0] as CallExpression
}
const props = codegenNode.arguments[1] as
@@ -68,7 +68,7 @@ function walk(
function getPatchFlag(node: ElementNode): number | undefined {
let codegenNode = node.codegenNode as CallExpression
if (codegenNode.callee.includes(APPLY_DIRECTIVES)) {
if (codegenNode.callee === APPLY_DIRECTIVES) {
codegenNode = codegenNode.arguments[0] as CallExpression
}
const flag = codegenNode.arguments[3]

View File

@@ -25,12 +25,10 @@ import {
RESOLVE_COMPONENT,
MERGE_PROPS,
TO_HANDLERS
} from '../runtimeConstants'
import { getInnerRange, isVSlot } from '../utils'
} from '../runtimeHelpers'
import { getInnerRange, isVSlot, toValidAssetId } from '../utils'
import { buildSlots } from './vSlot'
const toValidId = (str: string): string => str.replace(/[^\w]/g, '')
// generate a JavaScript AST for this element's codegen
export const transformElement: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
@@ -50,19 +48,14 @@ export const transformElement: NodeTransform = (node, context) => {
let patchFlag: number = 0
let runtimeDirectives: DirectiveNode[] | undefined
let dynamicPropNames: string[] | undefined
let componentIdentifier: string | undefined
if (isComponent) {
componentIdentifier = `_component_${toValidId(node.tag)}`
context.statements.add(
`const ${componentIdentifier} = ${context.helper(
RESOLVE_COMPONENT
)}(${JSON.stringify(node.tag)})`
)
context.helper(RESOLVE_COMPONENT)
context.components.add(node.tag)
}
const args: CallExpression['arguments'] = [
isComponent ? componentIdentifier! : `"${node.tag}"`
isComponent ? toValidAssetId(node.tag, `component`) : `"${node.tag}"`
]
// props
if (hasProps) {
@@ -402,13 +395,11 @@ function createDirectiveArgs(
context: TransformContext
): ArrayExpression {
// inject statement for resolving directive
const dirIdentifier = `_directive_${toValidId(dir.name)}`
context.statements.add(
`const ${dirIdentifier} = ${context.helper(
RESOLVE_DIRECTIVE
)}(${JSON.stringify(dir.name)})`
)
const dirArgs: ArrayExpression['elements'] = [dirIdentifier]
context.helper(RESOLVE_DIRECTIVE)
context.directives.add(dir.name)
const dirArgs: ArrayExpression['elements'] = [
toValidAssetId(dir.name, `directive`)
]
const { loc } = dir
if (dir.exp) dirArgs.push(dir.exp)
if (dir.arg) dirArgs.push(dir.arg)

View File

@@ -8,7 +8,7 @@ import {
import { isSlotOutlet } from '../utils'
import { buildProps } from './transformElement'
import { createCompilerError, ErrorCodes } from '../errors'
import { RENDER_SLOT } from '../runtimeConstants'
import { RENDER_SLOT } from '../runtimeHelpers'
export const transformSlotOutlet: NodeTransform = (node, context) => {
if (isSlotOutlet(node)) {

View File

@@ -2,7 +2,7 @@ import { DirectiveTransform } from '../transform'
import { createObjectProperty, createSimpleExpression, NodeTypes } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import { camelize } from '@vue/shared'
import { CAMELIZE } from '../runtimeConstants'
import { CAMELIZE } from '../runtimeHelpers'
// v-bind without arg is handled directly in ./element.ts due to it affecting
// codegen for the entire props object. This transform here is only for v-bind
@@ -20,10 +20,10 @@ export const transformBind: DirectiveTransform = (dir, context) => {
if (arg.isStatic) {
arg.content = camelize(arg.content)
} else {
arg.content = `${context.helper(CAMELIZE)}(${arg.content})`
arg.content = `${context.helperString(CAMELIZE)}(${arg.content})`
}
} else {
arg.children.unshift(`${context.helper(CAMELIZE)}(`)
arg.children.unshift(`${context.helperString(CAMELIZE)}(`)
arg.children.push(`)`)
}
}

View File

@@ -30,7 +30,7 @@ import {
OPEN_BLOCK,
CREATE_BLOCK,
FRAGMENT
} from '../runtimeConstants'
} from '../runtimeHelpers'
import { processExpression } from './transformExpression'
import { PatchFlags, PatchFlagNames } from '@vue/shared'
import { PropsExpression } from './transformElement'

View File

@@ -29,7 +29,7 @@ import {
APPLY_DIRECTIVES,
CREATE_VNODE,
RENDER_SLOT
} from '../runtimeConstants'
} from '../runtimeHelpers'
import { injectProp } from '../utils'
import { PropsExpression } from './transformElement'
@@ -184,18 +184,18 @@ function createChildrenCodegenNode(
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)) {
if (vnodeCall.callee === APPLY_DIRECTIVES) {
vnodeCall = vnodeCall.arguments[0] as CallExpression
}
// Change createVNode to createBlock.
if (vnodeCall.callee.includes(CREATE_VNODE)) {
if (vnodeCall.callee === CREATE_VNODE) {
vnodeCall.callee = helper(CREATE_BLOCK)
}
// It's possible to have renderSlot() here as well - which already produces
// a block, so no need to change the callee. However it accepts props at
// a different arg index so make sure to check for so that the key injection
// logic below works for it too.
const propsIndex = vnodeCall.callee.includes(RENDER_SLOT) ? 2 : 1
const propsIndex = vnodeCall.callee === RENDER_SLOT ? 2 : 1
// inject branch key
const existingProps = vnodeCall.arguments[propsIndex] as
| PropsExpression

View File

@@ -24,7 +24,7 @@ import {
import { TransformContext, NodeTransform } from '../transform'
import { createCompilerError, ErrorCodes } from '../errors'
import { findDir, isTemplateNode, assert, isVSlot } from '../utils'
import { CREATE_SLOTS, RENDER_LIST } from '../runtimeConstants'
import { CREATE_SLOTS, RENDER_LIST } from '../runtimeHelpers'
import { parseForExpression, createForLoopParams } from './vFor'
const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>

View File

@@ -19,7 +19,7 @@ import {
import { parse } from 'acorn'
import { walk } from 'estree-walker'
import { TransformContext } from './transform'
import { OPEN_BLOCK, CREATE_BLOCK, MERGE_PROPS } from './runtimeConstants'
import { OPEN_BLOCK, CREATE_BLOCK, MERGE_PROPS } from './runtimeHelpers'
import { isString, isFunction } from '@vue/shared'
import { PropsExpression } from './transforms/transformElement'
@@ -217,3 +217,10 @@ export function injectProp(
])
}
}
export function toValidAssetId(
name: string,
type: 'component' | 'directive'
): string {
return `_${type}_${name.replace(/[^\w]/g, '')}`
}