diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
index bf7631dbddc..8d2caf3cbf6 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
@@ -339,6 +339,52 @@ export function render(_ctx) {
}"
`;
+exports[`compiler v-bind > cache multiple access to the same expression 1`] = `
+"import { setProp as _setProp, setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("
")
+
+export function render(_ctx) {
+ const n0 = t0()
+ const n1 = t0()
+ const n2 = t0()
+ const n3 = t0()
+ const n4 = t0()
+ const n5 = t0()
+ const n6 = t0()
+ const n7 = t0()
+ const n8 = t0()
+ const n9 = t0()
+ const n10 = t0()
+ _renderEffect(() => {
+ const _obj = _ctx.obj
+ const _baz = _ctx.baz
+ const _foo = _ctx.foo
+ const _bar = _ctx.bar
+ const _key = _ctx.key
+ const _obj_foo_baz = _obj['foo']['baz']
+ const _obj_bar = _obj.bar
+ const _foo_bar_baz = _foo[_bar(_baz)]
+ const _obj_foo_baz_obj_bar = _obj_foo_baz + _obj_bar
+ const _foo_bar = _foo + _bar
+
+ _setProp(n0, "id", _obj_foo_baz_obj_bar)
+ _setProp(n1, "id", _obj_foo_baz_obj_bar)
+ _setProp(n2, "id", _obj[1][_baz] + _obj_bar)
+
+ _setProp(n3, "id", _foo_bar)
+ _setProp(n4, "id", _foo_bar)
+ _setProp(n5, "id", _foo + _foo + _bar)
+ _setProp(n6, "id", _foo)
+
+ _setProp(n7, "id", _foo_bar_baz)
+ _setProp(n8, "id", _foo_bar_baz)
+ _setProp(n9, "id", _bar() + _foo)
+ _setDynamicProps(n10, [{ [_key+1]: _foo[_key+1]() }])
+ })
+ return [n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10]
+}"
+`;
+
exports[`compiler v-bind > dynamic arg 1`] = `
"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("", true)
diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
index 4273b4bff01..ed928001a8d 100644
--- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
@@ -693,4 +693,21 @@ describe('compiler v-bind', () => {
)
expect(code).matchSnapshot()
})
+
+ test('cache multiple access to the same expression', () => {
+ const { code } = compileWithVBind(`
+
+
+
+
+
+
+
+
+
+
+
+ `)
+ expect(code).matchSnapshot()
+ })
})
diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts
index 903ebde4ee7..b093b5cff8c 100644
--- a/packages/compiler-vapor/src/generators/operation.ts
+++ b/packages/compiler-vapor/src/generators/operation.ts
@@ -22,7 +22,7 @@ import {
type SimpleExpressionNode,
createSimpleExpression,
} from '@vue/compiler-core'
-import { extend, NOOP } from '@vue/shared'
+import { NOOP, extend } from '@vue/shared'
import { genExpression } from './expression'
import { walk } from 'estree-walker'
import type { Node } from '@babel/types'
@@ -151,33 +151,31 @@ function genDeclarations(
context: CodegenContext,
): [Record, CodeFragment[]] {
const [frag, push] = buildCodeFragment()
- const ids = declarations.reduce(
- (acc, cur) => {
- if (!cur.replacement.includes('_')) {
- acc[cur.replacement] = `_${cur.replacement}`
- push(
- `const _${cur.replacement} = `,
- ...genExpression(cur.value, context),
- NEWLINE,
- )
- }
- return acc
- },
- {} as Record,
- )
+ const ids: Record = {}
- for (let i = 0; i < declarations.length; i++) {
- const { replacement, value } = declarations[i]
- const isExp = replacement.includes('_')
- if (isExp) {
- ids[replacement] = `_${replacement}`
- const descareFrag = context.withId(
- () => genExpression(value, context),
- isExp ? ids : {},
+ // process identifiers first
+ declarations.forEach(({ replacement, value }) => {
+ if (!replacement.includes('_')) {
+ const prefixedName = `_${replacement}`
+ ids[replacement] = prefixedName
+ push(
+ `const ${prefixedName} = `,
+ ...genExpression(value, context),
+ NEWLINE,
)
- push(`const _${replacement} = `, ...descareFrag, NEWLINE)
}
- }
+ })
+
+ // process expressions with potential identifier references
+ declarations.forEach(({ replacement, value }) => {
+ if (replacement.includes('_')) {
+ const prefixedName = `_${replacement}`
+ ids[replacement] = prefixedName
+ const expFrag = context.withId(() => genExpression(value, context), ids)
+ push(`const ${prefixedName} = `, ...expFrag, NEWLINE)
+ }
+ })
+
return [ids, frag]
}
@@ -186,29 +184,81 @@ function escapeRegExp(string: string) {
}
function processExpressions(context: CodegenContext): DeclarationValue[] {
- const plugins = context.options.expressionPlugins
- const options: BabelOptions = {
- plugins: plugins ? [...plugins, 'typescript'] : ['typescript'],
- }
const {
block: { expressions },
} = context
+ // 1. extract identifiers and member expressions
+ const { seenExp, replaceMap, memberExpMap } =
+ extractRepeatedExpressions(expressions)
+ // 2. handle identifiers and member expressions that appear more than once
+ // foo + obj.bar -> _foo + _obj.bar
+ const declarations = processRepeatedExpressions(
+ context,
+ seenExp,
+ replaceMap,
+ memberExpMap,
+ )
+ // 3. after processing identifiers and member expressions, remaining expressions may still contain duplicates
+ // for example: `_foo + _obj.bar` may appear multiple times.
+ // `_foo + _obj.bar` -> `_foo_obj_bar`
+ processRemainingExpressions(context, expressions, declarations)
+
+ return declarations
+}
+
+function extractRepeatedExpressions(expressions: SimpleExpressionNode[]) {
const seenExp: Record = Object.create(null)
const replaceMap = new Map>()
- const expMap = new Map()
+ const memberExpMap = new Map()
+
+ for (const exp of expressions) {
+ const addToMaps = (name: string) => {
+ seenExp[name] = (seenExp[name] || 0) + 1
+ const set = replaceMap.get(name) || new Set()
+ set.add(exp)
+ replaceMap.set(name, set)
+ }
+
+ if (!exp.ast) {
+ addToMaps(exp.content)
+ continue
+ }
+
+ walk(exp.ast, {
+ enter(currentNode: Node) {
+ if (currentNode.type === 'MemberExpression') {
+ const memberExp = getMemberExp(currentNode, addToMaps)
+ addToMaps(memberExp)
+ memberExpMap.set(memberExp, currentNode)
+ return this.skip()
+ }
+
+ if (currentNode.type === 'Identifier') {
+ addToMaps(currentNode.name)
+ }
+ },
+ })
+ }
+
+ return { seenExp, replaceMap, memberExpMap }
+}
+
+function processRepeatedExpressions(
+ context: CodegenContext,
+ seenExp: Record,
+ replaceMap: Map>,
+ memberExpMap: Map,
+): DeclarationValue[] {
const declarations: DeclarationValue[] = []
- expressions.forEach(exp =>
- extractExpression(exp, seenExp, replaceMap, expMap),
- )
for (const [key, values] of replaceMap) {
if (seenExp[key]! > 1 && values.size > 0) {
const varName = getReplacementName(key)
- const isExp = expMap.has(key)
+ const isMemberExp = memberExpMap.has(key)
if (!declarations.some(d => d.replacement === varName)) {
declarations.push({
replacement: varName,
value: extend(
- { ast: isExp ? parseExpression(`(${key})`, options) : null },
+ { ast: isMemberExp ? parseExp(context, key) : null },
createSimpleExpression(key),
),
})
@@ -216,11 +266,19 @@ function processExpressions(context: CodegenContext): DeclarationValue[] {
const replaceRE = new RegExp(escapeRegExp(key), 'g')
values.forEach(node => {
node.content = node.content.replace(replaceRE, varName)
- node.ast = parseExpression(`(${node.content})`, options)
+ node.ast = parseExp(context, node.content)
})
}
}
+ return declarations
+}
+
+function processRemainingExpressions(
+ context: CodegenContext,
+ expressions: SimpleExpressionNode[],
+ declarations: DeclarationValue[],
+): void {
const seenContent: Record = Object.create(null)
expressions.forEach(exp => {
if (exp.ast && exp.ast.type !== 'Identifier') {
@@ -236,18 +294,26 @@ function processExpressions(context: CodegenContext): DeclarationValue[] {
const varName = getReplacementName(exp.content)
const oldContent = exp.content
exp.content = varName
- exp.ast = parseExpression(`(${exp.content})`, options)
+ exp.ast = parseExp(context, exp.content)
if (!declarations.some(d => d.replacement === varName)) {
declarations.push({
replacement: varName,
- value: extend({ ast: parseExpression(`(${oldContent})`, options) }, createSimpleExpression(oldContent)),
+ value: extend(
+ { ast: parseExp(context, oldContent) },
+ createSimpleExpression(oldContent),
+ ),
})
}
}
})
+}
- // console.log({seenExp, replaceMap, declarations, expressions})
- return declarations
+function parseExp(context: CodegenContext, content: string): Node {
+ const plugins = context.options.expressionPlugins
+ const options: BabelOptions = {
+ plugins: plugins ? [...plugins, 'typescript'] : ['typescript'],
+ }
+ return parseExpression(`(${content})`, options)
}
function getReplacementName(name: string): string {
@@ -257,35 +323,6 @@ function getReplacementName(name: string): string {
.replace(/_+$/, '')}`
}
-function extractExpression(
- node: SimpleExpressionNode,
- seenExp: Record,
- replaceMap: Map>,
- expMap: Map,
-) {
- const add = (name: string) => {
- seenExp[name] = (seenExp[name] || 0) + 1
- replaceMap.set(name, (replaceMap.get(name) ?? new Set()).add(node))
- }
- if (node.ast) {
- walk(node.ast, {
- enter(node: Node) {
- if (node.type === 'MemberExpression') {
- const exp = getMemberExp(node, add)
- add(exp)
- expMap.set(exp, node)
- return this.skip()
- }
- if (node.type === 'Identifier') {
- add(node.name)
- }
- },
- })
- } else if (node.ast === null) {
- add((node as SimpleExpressionNode).content)
- }
-}
-
function getMemberExp(
expr: Node,
onIdentifier: (name: string) => void,