wip(compiler): property deduping

This commit is contained in:
Evan You 2019-09-25 12:39:46 -04:00
parent f79433cbb3
commit 3e8cd3f25f
7 changed files with 111 additions and 41 deletions

View File

@ -3,7 +3,8 @@ import {
CompilerOptions, CompilerOptions,
parse, parse,
transform, transform,
ErrorCodes ErrorCodes,
compile
} from '../../src' } from '../../src'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
import { import {
@ -305,10 +306,7 @@ describe('compiler: element transform', () => {
foo(dir) { foo(dir) {
_dir = dir _dir = dir
return { return {
props: [ props: [createObjectProperty(dir.arg!, dir.exp!, dir.loc)],
createObjectProperty(dir.arg!, dir.exp!, dir.loc),
createObjectProperty(dir.arg!, dir.exp!, dir.loc)
],
needRuntime: true needRuntime: true
} }
} }
@ -328,11 +326,6 @@ describe('compiler: element transform', () => {
{ {
type: NodeTypes.JS_OBJECT_EXPRESSION, type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [ properties: [
{
type: NodeTypes.JS_PROPERTY,
key: _dir!.arg,
value: _dir!.exp
},
{ {
type: NodeTypes.JS_PROPERTY, type: NodeTypes.JS_PROPERTY,
key: _dir!.arg, key: _dir!.arg,
@ -457,5 +450,12 @@ describe('compiler: element transform', () => {
]) ])
}) })
test('props dedupe', () => {
const { code } = compile(
`<div class="a" :class="b" @click.foo="a" @click.bar="b" style="color: red" :style="{fontSize: 14}" />`
)
console.log(code)
})
test.todo('slot outlets') test.todo('slot outlets')
}) })

View File

@ -159,7 +159,7 @@ export interface ObjectExpression extends Node {
export interface Property extends Node { export interface Property extends Node {
type: NodeTypes.JS_PROPERTY type: NodeTypes.JS_PROPERTY
key: ExpressionNode key: ExpressionNode
value: ExpressionNode value: JSChildNode
} }
export interface ArrayExpression extends Node { export interface ArrayExpression extends Node {

View File

@ -453,7 +453,7 @@ function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
genExpressionAsPropertyKey(key, context) genExpressionAsPropertyKey(key, context)
push(`: `) push(`: `)
// value // value
genExpression(value, context) genNode(value, context)
if (i < properties.length - 1) { if (i < properties.length - 1) {
// will only reach this if it's multilines // will only reach this if it's multilines
push(`,`) push(`,`)

View File

@ -12,7 +12,8 @@ import {
createArrayExpression, createArrayExpression,
createObjectProperty, createObjectProperty,
createExpression, createExpression,
createObjectExpression createObjectExpression,
Property
} from '../ast' } from '../ast'
import { isArray } from '@vue/shared' import { isArray } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
@ -135,7 +136,9 @@ function buildProps(
if (!arg && (isBind || name === 'on')) { if (!arg && (isBind || name === 'on')) {
if (exp) { if (exp) {
if (properties.length) { if (properties.length) {
mergeArgs.push(createObjectExpression(properties, elementLoc)) mergeArgs.push(
createObjectExpression(dedupeProperties(properties), elementLoc)
)
properties = [] properties = []
} }
if (isBind) { if (isBind) {
@ -187,7 +190,9 @@ function buildProps(
// has v-bind="object" or v-on="object", wrap with mergeProps // has v-bind="object" or v-on="object", wrap with mergeProps
if (mergeArgs.length) { if (mergeArgs.length) {
if (properties.length) { if (properties.length) {
mergeArgs.push(createObjectExpression(properties, elementLoc)) mergeArgs.push(
createObjectExpression(dedupeProperties(properties), elementLoc)
)
} }
if (mergeArgs.length > 1) { if (mergeArgs.length > 1) {
context.imports.add(MERGE_PROPS) context.imports.add(MERGE_PROPS)
@ -197,7 +202,10 @@ function buildProps(
propsExpression = mergeArgs[0] propsExpression = mergeArgs[0]
} }
} else { } else {
propsExpression = createObjectExpression(properties, elementLoc) propsExpression = createObjectExpression(
dedupeProperties(properties),
elementLoc
)
} }
return { return {
@ -206,6 +214,86 @@ function buildProps(
} }
} }
// Dedupe props in an object literal.
// Literal duplicated attributes would have been warned during the parse phase,
// however, it's possible to encounter duplicated `onXXX` handlers with different
// modifiers. We also need to merge static and dynamic class / style attributes.
// - onXXX handlers / style: merge into array
// - class: merge into single expression with concatenation
function dedupeProperties(properties: Property[]): Property[] {
const knownProps: Record<string, Property> = {}
const deduped: Property[] = []
for (let i = 0; i < properties.length; i++) {
const prop = properties[i]
// dynamic key named are always allowed
if (!prop.key.isStatic) {
deduped.push(prop)
continue
}
const name = prop.key.content
const existing = knownProps[name]
if (existing) {
if (name.startsWith('on')) {
mergeAsArray(existing, prop)
} else if (name === 'style') {
mergeStyles(existing, prop)
} else if (name === 'class') {
mergeClasses(existing, prop)
}
// unexpected duplicate, should have emitted error during parse
} else {
knownProps[name] = prop
deduped.push(prop)
}
}
return deduped
}
function mergeAsArray(existing: Property, incoming: Property) {
if (existing.value.type === NodeTypes.JS_ARRAY_EXPRESSION) {
existing.value.elements.push(incoming.value)
} else {
existing.value = createArrayExpression(
[existing.value, incoming.value],
existing.loc
)
}
}
// 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
function mergeClasses(existing: Property, incoming: Property) {
const e = existing.value as ExpressionNode
const children =
e.children ||
(e.children = [
{
...e,
children: undefined
}
])
// :class="expression" class="string"
// -> class: expression + "string"
children.push(` + " " + `, incoming.value as ExpressionNode)
}
function createDirectiveArgs( function createDirectiveArgs(
dir: DirectiveNode, dir: DirectiveNode,
context: TransformContext context: TransformContext

View File

@ -14,7 +14,6 @@ import { NodeTransform, TransformContext } from '../transform'
import { NodeTypes, createExpression, ExpressionNode } from '../ast' import { NodeTypes, createExpression, ExpressionNode } from '../ast'
import { Node, Function, Identifier } from 'estree' import { Node, Function, Identifier } from 'estree'
import { advancePositionWithClone } from '../utils' import { advancePositionWithClone } from '../utils'
export const transformExpression: NodeTransform = (node, context) => { export const transformExpression: NodeTransform = (node, context) => {
if (node.type === NodeTypes.EXPRESSION && !node.isStatic) { if (node.type === NodeTypes.EXPRESSION && !node.isStatic) {
processExpression(node, context) processExpression(node, context)
@ -27,9 +26,15 @@ export const transformExpression: NodeTransform = (node, context) => {
processExpression(prop.exp, context) processExpression(prop.exp, context)
} }
if (prop.arg && !prop.arg.isStatic) { if (prop.arg && !prop.arg.isStatic) {
if (prop.name === 'class') {
// TODO special expression optimization for classes
} else {
processExpression(prop.arg, context) processExpression(prop.arg, context)
} }
} }
} else if (prop.name === 'style') {
// TODO parse inline CSS literals into objects
}
} }
} }
} }

View File

@ -1,9 +0,0 @@
// Optimizations
// - b -> b (use runtime normalization)
// - ['foo', b] -> 'foo' + normalize(b)
// - { a, b: c } -> (a ? a : '') + (b ? c : '')
// - ['a', b, { c }] -> 'a' + normalize(b) + (c ? c : '')
// Also merge dynamic and static class into a single prop
// Attach CLASS patchFlag if necessary

View File

@ -1,14 +0,0 @@
// Optimizations
// The compiler pre-compiles static string styles into static objects
// + detects and hoists inline static objects
// e.g. `style="color: red"` and `:style="{ color: 'red' }"` both get hoisted as
// ``` js
// const style = { color: 'red' }
// render() { return e('div', { style }) }
// ```
// Also nerge dynamic and static style into a single prop
// Attach STYLE patchFlag if necessary