Skip to content

Commit 3e8cd3f

Browse files
committed
wip(compiler): property deduping
1 parent f79433c commit 3e8cd3f

File tree

7 files changed

+111
-41
lines changed

7 files changed

+111
-41
lines changed

packages/compiler-core/__tests__/transforms/transformElement.spec.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
CompilerOptions,
44
parse,
55
transform,
6-
ErrorCodes
6+
ErrorCodes,
7+
compile
78
} from '../../src'
89
import { transformElement } from '../../src/transforms/transformElement'
910
import {
@@ -305,10 +306,7 @@ describe('compiler: element transform', () => {
305306
foo(dir) {
306307
_dir = dir
307308
return {
308-
props: [
309-
createObjectProperty(dir.arg!, dir.exp!, dir.loc),
310-
createObjectProperty(dir.arg!, dir.exp!, dir.loc)
311-
],
309+
props: [createObjectProperty(dir.arg!, dir.exp!, dir.loc)],
312310
needRuntime: true
313311
}
314312
}
@@ -328,11 +326,6 @@ describe('compiler: element transform', () => {
328326
{
329327
type: NodeTypes.JS_OBJECT_EXPRESSION,
330328
properties: [
331-
{
332-
type: NodeTypes.JS_PROPERTY,
333-
key: _dir!.arg,
334-
value: _dir!.exp
335-
},
336329
{
337330
type: NodeTypes.JS_PROPERTY,
338331
key: _dir!.arg,
@@ -457,5 +450,12 @@ describe('compiler: element transform', () => {
457450
])
458451
})
459452

453+
test('props dedupe', () => {
454+
const { code } = compile(
455+
`<div class="a" :class="b" @click.foo="a" @click.bar="b" style="color: red" :style="{fontSize: 14}" />`
456+
)
457+
console.log(code)
458+
})
459+
460460
test.todo('slot outlets')
461461
})

packages/compiler-core/src/ast.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export interface ObjectExpression extends Node {
159159
export interface Property extends Node {
160160
type: NodeTypes.JS_PROPERTY
161161
key: ExpressionNode
162-
value: ExpressionNode
162+
value: JSChildNode
163163
}
164164

165165
export interface ArrayExpression extends Node {

packages/compiler-core/src/codegen.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
453453
genExpressionAsPropertyKey(key, context)
454454
push(`: `)
455455
// value
456-
genExpression(value, context)
456+
genNode(value, context)
457457
if (i < properties.length - 1) {
458458
// will only reach this if it's multilines
459459
push(`,`)

packages/compiler-core/src/transforms/transformElement.ts

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
createArrayExpression,
1313
createObjectProperty,
1414
createExpression,
15-
createObjectExpression
15+
createObjectExpression,
16+
Property
1617
} from '../ast'
1718
import { isArray } from '@vue/shared'
1819
import { createCompilerError, ErrorCodes } from '../errors'
@@ -135,7 +136,9 @@ function buildProps(
135136
if (!arg && (isBind || name === 'on')) {
136137
if (exp) {
137138
if (properties.length) {
138-
mergeArgs.push(createObjectExpression(properties, elementLoc))
139+
mergeArgs.push(
140+
createObjectExpression(dedupeProperties(properties), elementLoc)
141+
)
139142
properties = []
140143
}
141144
if (isBind) {
@@ -187,7 +190,9 @@ function buildProps(
187190
// has v-bind="object" or v-on="object", wrap with mergeProps
188191
if (mergeArgs.length) {
189192
if (properties.length) {
190-
mergeArgs.push(createObjectExpression(properties, elementLoc))
193+
mergeArgs.push(
194+
createObjectExpression(dedupeProperties(properties), elementLoc)
195+
)
191196
}
192197
if (mergeArgs.length > 1) {
193198
context.imports.add(MERGE_PROPS)
@@ -197,7 +202,10 @@ function buildProps(
197202
propsExpression = mergeArgs[0]
198203
}
199204
} else {
200-
propsExpression = createObjectExpression(properties, elementLoc)
205+
propsExpression = createObjectExpression(
206+
dedupeProperties(properties),
207+
elementLoc
208+
)
201209
}
202210

203211
return {
@@ -206,6 +214,86 @@ function buildProps(
206214
}
207215
}
208216

217+
// Dedupe props in an object literal.
218+
// Literal duplicated attributes would have been warned during the parse phase,
219+
// however, it's possible to encounter duplicated `onXXX` handlers with different
220+
// modifiers. We also need to merge static and dynamic class / style attributes.
221+
// - onXXX handlers / style: merge into array
222+
// - class: merge into single expression with concatenation
223+
function dedupeProperties(properties: Property[]): Property[] {
224+
const knownProps: Record<string, Property> = {}
225+
const deduped: Property[] = []
226+
for (let i = 0; i < properties.length; i++) {
227+
const prop = properties[i]
228+
// dynamic key named are always allowed
229+
if (!prop.key.isStatic) {
230+
deduped.push(prop)
231+
continue
232+
}
233+
const name = prop.key.content
234+
const existing = knownProps[name]
235+
if (existing) {
236+
if (name.startsWith('on')) {
237+
mergeAsArray(existing, prop)
238+
} else if (name === 'style') {
239+
mergeStyles(existing, prop)
240+
} else if (name === 'class') {
241+
mergeClasses(existing, prop)
242+
}
243+
// unexpected duplicate, should have emitted error during parse
244+
} else {
245+
knownProps[name] = prop
246+
deduped.push(prop)
247+
}
248+
}
249+
return deduped
250+
}
251+
252+
function mergeAsArray(existing: Property, incoming: Property) {
253+
if (existing.value.type === NodeTypes.JS_ARRAY_EXPRESSION) {
254+
existing.value.elements.push(incoming.value)
255+
} else {
256+
existing.value = createArrayExpression(
257+
[existing.value, incoming.value],
258+
existing.loc
259+
)
260+
}
261+
}
262+
263+
// Merge dynamic and static style into a single prop
264+
export function mergeStyles(existing: Property, incoming: Property) {
265+
if (
266+
existing.value.type === NodeTypes.JS_OBJECT_EXPRESSION &&
267+
incoming.value.type === NodeTypes.JS_OBJECT_EXPRESSION
268+
) {
269+
// if both are objects, merge the object expressions.
270+
// style="color: red" :style="{ a: b }"
271+
// -> { color: "red", a: b }
272+
existing.value.properties.push(...incoming.value.properties)
273+
} else {
274+
// otherwise merge as array
275+
// style="color:red" :style="a"
276+
// -> style: [{ color: "red" }, a]
277+
mergeAsArray(existing, incoming)
278+
}
279+
}
280+
281+
// Merge dynamic and static class into a single prop
282+
function mergeClasses(existing: Property, incoming: Property) {
283+
const e = existing.value as ExpressionNode
284+
const children =
285+
e.children ||
286+
(e.children = [
287+
{
288+
...e,
289+
children: undefined
290+
}
291+
])
292+
// :class="expression" class="string"
293+
// -> class: expression + "string"
294+
children.push(` + " " + `, incoming.value as ExpressionNode)
295+
}
296+
209297
function createDirectiveArgs(
210298
dir: DirectiveNode,
211299
context: TransformContext

packages/compiler-core/src/transforms/transformExpression.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { NodeTransform, TransformContext } from '../transform'
1414
import { NodeTypes, createExpression, ExpressionNode } from '../ast'
1515
import { Node, Function, Identifier } from 'estree'
1616
import { advancePositionWithClone } from '../utils'
17-
1817
export const transformExpression: NodeTransform = (node, context) => {
1918
if (node.type === NodeTypes.EXPRESSION && !node.isStatic) {
2019
processExpression(node, context)
@@ -27,8 +26,14 @@ export const transformExpression: NodeTransform = (node, context) => {
2726
processExpression(prop.exp, context)
2827
}
2928
if (prop.arg && !prop.arg.isStatic) {
30-
processExpression(prop.arg, context)
29+
if (prop.name === 'class') {
30+
// TODO special expression optimization for classes
31+
} else {
32+
processExpression(prop.arg, context)
33+
}
3134
}
35+
} else if (prop.name === 'style') {
36+
// TODO parse inline CSS literals into objects
3237
}
3338
}
3439
}

packages/compiler-core/src/transforms/vBindClass.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.

packages/compiler-core/src/transforms/vBindStyle.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)