feat(compiler): transformStyle + context.hoist

This commit is contained in:
Evan You
2019-09-25 14:13:33 -04:00
parent 3e8cd3f25f
commit b43f3b61b7
13 changed files with 290 additions and 59 deletions

View File

@@ -68,6 +68,7 @@ export interface RootNode extends Node {
children: ChildNode[]
imports: string[]
statements: string[]
hoists: JSChildNode[]
}
export interface ElementNode extends Node {

View File

@@ -148,14 +148,16 @@ export function generate(
if (mode === 'function') {
// generate const declarations for helpers
if (imports) {
push(`const { ${imports} } = Vue\n\n`)
push(`const { ${imports} } = Vue\n`)
}
genHoists(ast.hoists, context)
push(`return `)
} else {
// generate import statements for helpers
if (imports) {
push(`import { ${imports} } from 'vue'\n\n`)
push(`import { ${imports} } from 'vue'\n`)
}
genHoists(ast.hoists, context)
push(`export default `)
}
push(`function render() {`)
@@ -190,6 +192,15 @@ export function generate(
}
}
function genHoists(hoists: JSChildNode[], context: CodegenContext) {
hoists.forEach((exp, i) => {
context.push(`const _hoisted_${i + 1} = `)
genNode(exp, context)
context.newline()
})
context.newline()
}
// This will generate a single vnode call if:
// - The list has length === 1, AND:
// - This is a root node, OR:

View File

@@ -10,6 +10,7 @@ import { transformOn } from './transforms/vOn'
import { transformBind } from './transforms/vBind'
import { transformExpression } from './transforms/transformExpression'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
import { transformStyle } from './transforms/transformStyle'
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
@@ -33,6 +34,7 @@ export function compile(
transformIf,
transformFor,
...(prefixIdentifiers ? [transformExpression] : []),
transformStyle,
transformElement,
...(options.nodeTransforms || []) // user transforms
],
@@ -53,7 +55,7 @@ export {
createStructuralDirectiveTransform,
TransformOptions,
TransformContext,
NodeTransform as Transform,
NodeTransform,
StructuralDirectiveTransform
} from './transform'
export {
@@ -64,8 +66,3 @@ export {
} from './codegen'
export { ErrorCodes, CompilerError, createCompilerError } from './errors'
export * from './ast'
// debug
export {
transformElement as prepareElementForCodegen
} from './transforms/transformElement'

View File

@@ -84,6 +84,7 @@ export function parse(content: string, options: ParserOptions = {}): RootNode {
children: parseChildren(context, TextModes.DATA, []),
imports: [],
statements: [],
hoists: [],
loc: getSelection(context, start)
}
}

View File

@@ -6,7 +6,9 @@ import {
ElementNode,
DirectiveNode,
Property,
ExpressionNode
ExpressionNode,
createExpression,
JSChildNode
} from './ast'
import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors'
@@ -52,6 +54,7 @@ export interface TransformContext extends Required<TransformOptions> {
root: RootNode
imports: Set<string>
statements: string[]
hoists: JSChildNode[]
identifiers: { [name: string]: number | undefined }
parent: ParentNode
childIndex: number
@@ -61,6 +64,7 @@ export interface TransformContext extends Required<TransformOptions> {
onNodeRemoved: () => void
addIdentifier(exp: ExpressionNode): void
removeIdentifier(exp: ExpressionNode): void
hoist(exp: JSChildNode): ExpressionNode
}
function createTransformContext(
@@ -76,6 +80,7 @@ function createTransformContext(
root,
imports: new Set(),
statements: [],
hoists: [],
identifiers: {},
prefixIdentifiers,
nodeTransforms,
@@ -125,6 +130,14 @@ function createTransformContext(
},
removeIdentifier({ content }) {
;(context.identifiers[content] as number)--
},
hoist(exp) {
context.hoists.push(exp)
return createExpression(
`_hoisted_${context.hoists.length}`,
false,
exp.loc
)
}
}
return context
@@ -135,6 +148,7 @@ export function transform(root: RootNode, options: TransformOptions) {
traverseChildren(root, context)
root.imports = [...context.imports]
root.statements = context.statements
root.hoists = context.hoists
}
export function traverseChildren(

View File

@@ -108,6 +108,7 @@ function buildProps(
props: PropsExpression
directives: DirectiveNode[]
} {
let isStatic = true
let properties: ObjectExpression['properties'] = []
const mergeArgs: PropsExpression[] = []
const runtimeDirectives: DirectiveNode[] = []
@@ -130,6 +131,7 @@ function buildProps(
)
} else {
// directives
isStatic = false
const { name, arg, exp, loc } = prop
// special case for v-bind and v-on with no argument
const isBind = name === 'bind'
@@ -208,6 +210,11 @@ function buildProps(
)
}
// hoist the object if it's fully static
if (isStatic) {
propsExpression = context.hoist(propsExpression)
}
return {
props: propsExpression,
directives: runtimeDirectives
@@ -233,10 +240,8 @@ function dedupeProperties(properties: Property[]): Property[] {
const name = prop.key.content
const existing = knownProps[name]
if (existing) {
if (name.startsWith('on')) {
if (name.startsWith('on') || name === 'style') {
mergeAsArray(existing, prop)
} else if (name === 'style') {
mergeStyles(existing, prop)
} else if (name === 'class') {
mergeClasses(existing, prop)
}
@@ -260,25 +265,9 @@ function mergeAsArray(existing: Property, incoming: Property) {
}
}
// Merge dynamic and static style into a single prop
export function mergeStyles(existing: Property, incoming: Property) {
if (
existing.value.type === NodeTypes.JS_OBJECT_EXPRESSION &&
incoming.value.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
// if both are objects, merge the object expressions.
// style="color: red" :style="{ a: b }"
// -> { color: "red", a: b }
existing.value.properties.push(...incoming.value.properties)
} else {
// otherwise merge as array
// style="color:red" :style="a"
// -> style: [{ color: "red" }, a]
mergeAsArray(existing, incoming)
}
}
// Merge dynamic and static class into a single prop
// :class="expression" class="string"
// -> class: expression + "string"
function mergeClasses(existing: Property, incoming: Property) {
const e = existing.value as ExpressionNode
const children =
@@ -289,8 +278,6 @@ function mergeClasses(existing: Property, incoming: Property) {
children: undefined
}
])
// :class="expression" class="string"
// -> class: expression + "string"
children.push(` + " " + `, incoming.value as ExpressionNode)
}

View File

@@ -28,12 +28,11 @@ export const transformExpression: NodeTransform = (node, context) => {
if (prop.arg && !prop.arg.isStatic) {
if (prop.name === 'class') {
// TODO special expression optimization for classes
processExpression(prop.arg, context)
} else {
processExpression(prop.arg, context)
}
}
} else if (prop.name === 'style') {
// TODO parse inline CSS literals into objects
}
}
}

View File

@@ -0,0 +1,37 @@
import { NodeTransform } from '../transform'
import { NodeTypes, createExpression } from '../ast'
// prase inline CSS strings for static style attributes into an object
export const transformStyle: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
node.props.forEach((p, i) => {
if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) {
// replace p with an expression node
const parsed = JSON.stringify(parseInlineCSS(p.value.content))
const exp = context.hoist(createExpression(parsed, false, p.loc))
node.props[i] = {
type: NodeTypes.DIRECTIVE,
name: `bind`,
arg: createExpression(`style`, true, p.loc),
exp,
modifiers: [],
loc: p.loc
}
}
})
}
}
const listDelimiterRE = /;(?![^(]*\))/g
const propertyDelimiterRE = /:(.+)/
function parseInlineCSS(cssText: string): Record<string, string> {
const res: Record<string, string> = {}
cssText.split(listDelimiterRE).forEach(function(item) {
if (item) {
const tmp = item.split(propertyDelimiterRE)
tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim())
}
})
return res
}