From: edison Date: Wed, 8 Jan 2025 07:05:48 +0000 (+0800) Subject: refactor(compiler-vapor): cache multiple access to the same expression (#12568) X-Git-Tag: v3.6.0-alpha.1~16^2~140 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=58b49749c7c076ed49b0bd99ad167c37d58d7cf2;p=thirdparty%2Fvuejs%2Fcore.git refactor(compiler-vapor): cache multiple access to the same expression (#12568) --- diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index fad91a7952..2d553fcea8 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -182,8 +182,9 @@ export function render(_ctx) { const n0 = t0() _delegate(n0, "click", () => _ctx.handleClick) _renderEffect(() => { - _setText(n0, _ctx.count, "foo", _ctx.count, "foo", _ctx.count) - _setProp(n0, "id", _ctx.count) + const _count = _ctx.count + _setText(n0, _count, "foo", _count, "foo", _count) + _setProp(n0, "id", _count) }) return n0 }" @@ -199,7 +200,10 @@ exports[`compile > expression parsing > interpolation 1`] = ` exports[`compile > expression parsing > v-bind 1`] = ` " const n0 = t0() - _renderEffect(() => _setDynamicProps(n0, [{ [key.value+1]: _unref(foo)[key.value+1]() }], true)) + _renderEffect(() => { + const _key = key.value + _setDynamicProps(n0, [{ [_key+1]: _unref(foo)[_key+1]() }], true) + }) return n0 " `; diff --git a/packages/compiler-vapor/__tests__/compile.spec.ts b/packages/compiler-vapor/__tests__/compile.spec.ts index 1699d35028..33f399caa7 100644 --- a/packages/compiler-vapor/__tests__/compile.spec.ts +++ b/packages/compiler-vapor/__tests__/compile.spec.ts @@ -193,9 +193,10 @@ describe('compile', () => { }, }) expect(code).matchSnapshot() - expect(code).contains('key.value+1') + expect(code).contains('const _key = key.value') + expect(code).contains('_key+1') expect(code).contains( - '_setDynamicProps(n0, [{ [key.value+1]: _unref(foo)[key.value+1]() }], true)', + '_setDynamicProps(n0, [{ [_key+1]: _unref(foo)[_key+1]() }], true)', ) }) 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 857b3a5d00..24585e39ea 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap @@ -1,5 +1,153 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`cache multiple access > dynamic key bindings with expressions 1`] = ` +"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + const _key = _ctx.key + _setDynamicProps(n0, [{ [_key+1]: _ctx.foo[_key+1]() }], true) + }) + return n0 +}" +`; + +exports[`cache multiple access > dynamic property access 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + const _obj = _ctx.obj + _setProp(n0, "id", _obj[1][_ctx.baz] + _obj.bar) + }) + return n0 +}" +`; + +exports[`cache multiple access > function calls with arguments 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + const n1 = t0() + const n2 = t0() + _renderEffect(() => { + const _foo = _ctx.foo + const _bar = _ctx.bar + const _foo_bar_baz = _foo[_bar(_ctx.baz)] + + _setProp(n0, "id", _foo_bar_baz) + _setProp(n1, "id", _foo_bar_baz) + _setProp(n2, "id", _bar() + _foo) + }) + return [n0, n1, n2] +}" +`; + +exports[`cache multiple access > not cache variable and member expression with the same name 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setProp(n0, "id", _ctx.bar + _ctx.obj.bar)) + return n0 +}" +`; + +exports[`cache multiple access > object property chain access 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + const n1 = t0() + _renderEffect(() => { + const _obj = _ctx.obj + const _obj_foo_baz_obj_bar = _obj['foo']['baz'] + _obj.bar + + _setProp(n0, "id", _obj_foo_baz_obj_bar) + _setProp(n1, "id", _obj_foo_baz_obj_bar) + }) + return [n0, n1] +}" +`; + +exports[`cache multiple access > repeated expression in expressions 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + const n1 = t0() + const n2 = t0() + _renderEffect(() => { + const _foo = _ctx.foo + const _foo_bar = _foo + _ctx.bar + + _setProp(n0, "id", _foo_bar) + _setProp(n1, "id", _foo_bar) + _setProp(n2, "id", _foo + _foo_bar) + }) + return [n0, n1, n2] +}" +`; + +exports[`cache multiple access > repeated expressions 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + const n1 = t0() + _renderEffect(() => { + const _foo_bar = _ctx.foo + _ctx.bar + + _setProp(n0, "id", _foo_bar) + _setProp(n1, "id", _foo_bar) + }) + return [n0, n1] +}" +`; + +exports[`cache multiple access > repeated variable in expressions 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + const n1 = t0() + _renderEffect(() => { + const _foo = _ctx.foo + _setProp(n0, "id", _foo + _foo + _ctx.bar) + _setProp(n1, "id", _foo) + }) + return [n0, n1] +}" +`; + +exports[`cache multiple access > repeated variables 1`] = ` +"import { setClass as _setClass, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = t0() + const n1 = t0() + _renderEffect(() => { + const _foo = _ctx.foo + + _setClass(n0, _foo) + _setClass(n1, _foo) + }) + return [n0, n1] +}" +`; + exports[`compiler v-bind > .attr modifier 1`] = ` "import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue'; const t0 = _template("
", true) @@ -305,6 +453,8 @@ export function render(_ctx) { const n5 = t5() const n6 = t6() _renderEffect(() => { + const _width = _ctx.width + const _height = _ctx.height _setAttr(n0, "spellcheck", _ctx.spellcheck) _setAttr(n0, "draggable", _ctx.draggable) _setAttr(n0, "translate", _ctx.translate) @@ -312,15 +462,15 @@ export function render(_ctx) { _setAttr(n1, "list", _ctx.list) _setAttr(n2, "type", _ctx.type) - _setAttr(n3, "width", _ctx.width) - _setAttr(n4, "width", _ctx.width) - _setAttr(n5, "width", _ctx.width) - _setAttr(n6, "width", _ctx.width) + _setAttr(n3, "width", _width) + _setAttr(n4, "width", _width) + _setAttr(n5, "width", _width) + _setAttr(n6, "width", _width) - _setAttr(n3, "height", _ctx.height) - _setAttr(n4, "height", _ctx.height) - _setAttr(n5, "height", _ctx.height) - _setAttr(n6, "height", _ctx.height) + _setAttr(n3, "height", _height) + _setAttr(n4, "height", _height) + _setAttr(n5, "height", _height) + _setAttr(n6, "height", _height) }) return [n0, n1, n2, n3, n4, n5, n6] }" @@ -343,7 +493,11 @@ const t0 = _template("
", true) export function render(_ctx) { const n0 = t0() - _renderEffect(() => _setDynamicProps(n0, [{ [_ctx.id]: _ctx.id, [_ctx.title]: _ctx.title }], true)) + _renderEffect(() => { + const _id = _ctx.id + const _title = _ctx.title + _setDynamicProps(n0, [{ [_id]: _id, [_title]: _title }], true) + }) return n0 }" `; @@ -354,7 +508,10 @@ const t0 = _template("
", true) export function render(_ctx) { const n0 = t0() - _renderEffect(() => _setDynamicProps(n0, [{ [_ctx.id]: _ctx.id, foo: "bar", checked: "" }], true)) + _renderEffect(() => { + const _id = _ctx.id + _setDynamicProps(n0, [{ [_id]: _id, foo: "bar", checked: "" }], true) + }) return n0 }" `; diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts index d785adfe81..5025997e20 100644 --- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts @@ -171,7 +171,7 @@ describe('compiler v-bind', () => { ], }) expect(code).contains( - '_setDynamicProps(n0, [{ [_ctx.id]: _ctx.id, [_ctx.title]: _ctx.title }], true)', + '_setDynamicProps(n0, [{ [_id]: _id, [_title]: _title }], true)', ) }) @@ -224,7 +224,7 @@ describe('compiler v-bind', () => { ], }) expect(code).contains( - '_setDynamicProps(n0, [{ [_ctx.id]: _ctx.id, foo: "bar", checked: "" }], true)', + '_setDynamicProps(n0, [{ [_id]: _id, foo: "bar", checked: "" }], true)', ) }) @@ -615,13 +615,13 @@ describe('compiler v-bind', () => { expect(code).contains('_setAttr(n0, "form", _ctx.form)') expect(code).contains('_setAttr(n1, "list", _ctx.list)') expect(code).contains('_setAttr(n2, "type", _ctx.type)') - expect(code).contains('_setAttr(n3, "width", _ctx.width)') - expect(code).contains('_setAttr(n3, "height", _ctx.height)') - expect(code).contains('_setAttr(n4, "width", _ctx.width)') - expect(code).contains('_setAttr(n4, "height", _ctx.height)') - expect(code).contains('_setAttr(n5, "width", _ctx.width)') - expect(code).contains('_setAttr(n5, "height", _ctx.height)') - expect(code).contains(' _setAttr(n6, "width", _ctx.width)') + expect(code).contains('_setAttr(n3, "width", _width)') + expect(code).contains('_setAttr(n3, "height", _height)') + expect(code).contains('_setAttr(n4, "width", _width)') + expect(code).contains('_setAttr(n4, "height", _height)') + expect(code).contains('_setAttr(n5, "width", _width)') + expect(code).contains('_setAttr(n5, "height", _height)') + expect(code).contains(' _setAttr(n6, "width", _width)') }) test(':innerHTML', () => { @@ -694,3 +694,102 @@ describe('compiler v-bind', () => { expect(code).matchSnapshot() }) }) + +describe('cache multiple access', () => { + test('repeated variables', () => { + const { code } = compileWithVBind(` +
+
+ `) + expect(code).matchSnapshot() + expect(code).contains('const _foo = _ctx.foo') + expect(code).contains('setClass(n0, _foo)') + expect(code).contains('setClass(n1, _foo)') + }) + + test('repeated expressions', () => { + const { code } = compileWithVBind(` +
+
+ `) + expect(code).matchSnapshot() + expect(code).contains('const _foo_bar = _ctx.foo + _ctx.bar') + expect(code).contains('_setProp(n0, "id", _foo_bar)') + expect(code).contains('_setProp(n1, "id", _foo_bar)') + }) + + test('repeated variable in expressions', () => { + const { code } = compileWithVBind(` +
+
+ `) + expect(code).matchSnapshot() + expect(code).contains('const _foo = _ctx.foo') + expect(code).contains('_setProp(n0, "id", _foo + _foo + _ctx.bar)') + expect(code).contains('_setProp(n1, "id", _foo)') + }) + + test('repeated expression in expressions', () => { + const { code } = compileWithVBind(` +
+
+
+ `) + expect(code).matchSnapshot() + expect(code).contains('const _foo_bar = _foo + _ctx.bar') + expect(code).contains('_setProp(n0, "id", _foo_bar)') + expect(code).contains('_setProp(n2, "id", _foo + _foo_bar)') + }) + + test('function calls with arguments', () => { + const { code } = compileWithVBind(` +
+
+
+ `) + expect(code).matchSnapshot() + expect(code).contains('const _foo_bar_baz = _foo[_bar(_ctx.baz)]') + expect(code).contains('_setProp(n0, "id", _foo_bar_baz)') + expect(code).contains('_setProp(n1, "id", _foo_bar_baz)') + expect(code).contains('_setProp(n2, "id", _bar() + _foo)') + }) + + test('dynamic key bindings with expressions', () => { + const { code } = compileWithVBind(` +
+ `) + expect(code).matchSnapshot() + expect(code).contains('const _key = _ctx.key') + expect(code).contains('[{ [_key+1]: _ctx.foo[_key+1]() }]') + }) + + test('object property chain access', () => { + const { code } = compileWithVBind(` +
+
+ `) + expect(code).matchSnapshot() + expect(code).contains( + "const _obj_foo_baz_obj_bar = _obj['foo']['baz'] + _obj.bar", + ) + expect(code).contains('_setProp(n0, "id", _obj_foo_baz_obj_bar)') + expect(code).contains('_setProp(n1, "id", _obj_foo_baz_obj_bar)') + }) + + test('dynamic property access', () => { + const { code } = compileWithVBind(` +
+ `) + expect(code).matchSnapshot() + expect(code).contains('const _obj = _ctx.obj') + expect(code).contains('_setProp(n0, "id", _obj[1][_ctx.baz] + _obj.bar)') + }) + + test('not cache variable and member expression with the same name', () => { + const { code } = compileWithVBind(` +
+ `) + expect(code).matchSnapshot() + expect(code).not.contains('const _bar = _ctx.bar') + }) +}) diff --git a/packages/compiler-vapor/src/generators/expression.ts b/packages/compiler-vapor/src/generators/expression.ts index 990659b84e..8e71458337 100644 --- a/packages/compiler-vapor/src/generators/expression.ts +++ b/packages/compiler-vapor/src/generators/expression.ts @@ -1,19 +1,21 @@ -import { genPropsAccessExp, isGloballyAllowed } from '@vue/shared' +import { NOOP, extend, genPropsAccessExp, isGloballyAllowed } from '@vue/shared' import { BindingTypes, NewlineType, type SimpleExpressionNode, type SourceLocation, advancePositionWithClone, + createSimpleExpression, isInDestructureAssignment, isStaticProperty, walkIdentifiers, } from '@vue/compiler-dom' -import type { Identifier } from '@babel/types' +import type { Identifier, Node } from '@babel/types' import type { CodegenContext } from '../generate' -import type { Node } from '@babel/types' import { isConstantExpression } from '../utils' -import { type CodeFragment, buildCodeFragment } from './utils' +import { type CodeFragment, NEWLINE, buildCodeFragment } from './utils' +import { walk } from 'estree-walker' +import { type ParserOptions, parseExpression } from '@babel/parser' export function genExpression( node: SimpleExpressionNode, @@ -209,3 +211,347 @@ function canPrefix(name: string) { return false return true } + +type DeclarationResult = { + ids: Record + frag: CodeFragment[] +} +type DeclarationValue = { + name: string + isIdentifier?: boolean + value: SimpleExpressionNode + rawName?: string + exps?: Set + seenCount?: number +} + +export function processExpressions( + context: CodegenContext, + expressions: SimpleExpressionNode[], +): DeclarationResult { + // analyze variables + const { seenVariable, variableToExpMap, expToVariableMap, seenIdentifier } = + analyzeExpressions(expressions) + + // process repeated identifiers and member expressions + // e.g., `foo[baz]` will be transformed into `foo_baz` + const varDeclarations = processRepeatedVariables( + context, + seenVariable, + variableToExpMap, + expToVariableMap, + seenIdentifier, + ) + + // process duplicate expressions after identifier and member expression handling. + // e.g., `foo + bar` will be transformed into `foo_bar` + const expDeclarations = processRepeatedExpressions( + context, + expressions, + varDeclarations, + ) + + return genDeclarations([...varDeclarations, ...expDeclarations], context) +} + +function analyzeExpressions(expressions: SimpleExpressionNode[]) { + const seenVariable: Record = Object.create(null) + const variableToExpMap = new Map>() + const expToVariableMap = new Map() + const seenIdentifier = new Set() + + const registerVariable = ( + name: string, + exp: SimpleExpressionNode, + isIdentifier: boolean, + ) => { + if (isIdentifier) seenIdentifier.add(name) + seenVariable[name] = (seenVariable[name] || 0) + 1 + variableToExpMap.set( + name, + (variableToExpMap.get(name) || new Set()).add(exp), + ) + expToVariableMap.set(exp, (expToVariableMap.get(exp) || []).concat(name)) + } + + for (const exp of expressions) { + if (!exp.ast) { + exp.ast === null && registerVariable(exp.content, exp, true) + continue + } + + walk(exp.ast, { + enter(currentNode: Node) { + if (currentNode.type === 'MemberExpression') { + const memberExp = extractMemberExpression( + currentNode, + (name: string) => { + registerVariable(name, exp, true) + }, + ) + registerVariable(memberExp, exp, false) + return this.skip() + } + + if (currentNode.type === 'Identifier') { + registerVariable(currentNode.name, exp, true) + } + }, + }) + } + + return { seenVariable, seenIdentifier, variableToExpMap, expToVariableMap } +} + +function processRepeatedVariables( + context: CodegenContext, + seenVariable: Record, + variableToExpMap: Map>, + expToVariableMap: Map, + seenIdentifier: Set, +): DeclarationValue[] { + const declarations: DeclarationValue[] = [] + for (const [name, exps] of variableToExpMap) { + if (seenVariable[name] > 1 && exps.size > 0) { + const isIdentifier = seenIdentifier.has(name) + const varName = isIdentifier ? name : genVarName(name) + + // replaces all non-identifiers with the new name. if node content + // includes only one member expression, it will become an identifier, + // e.g., foo[baz] -> foo_baz. + // for identifiers, we don't need to replace the content - they will be + // replaced during context.withId(..., ids) + const replaceRE = new RegExp(escapeRegExp(name), 'g') + exps.forEach(node => { + if (node.ast) { + node.content = node.content.replace(replaceRE, varName) + // re-parse the expression + node.ast = parseExp(context, node.content) + } + }) + + if ( + !declarations.some(d => d.name === varName) && + (!isIdentifier || shouldDeclareVariable(name, expToVariableMap, exps)) + ) { + declarations.push({ + name: varName, + isIdentifier, + value: extend( + { ast: isIdentifier ? null : parseExp(context, name) }, + createSimpleExpression(name), + ), + rawName: name, + exps, + seenCount: seenVariable[name], + }) + } + } + } + + return declarations +} + +function shouldDeclareVariable( + name: string, + expToVariableMap: Map, + exps: Set, +): boolean { + const vars = Array.from(exps, exp => expToVariableMap.get(exp)!) + // assume name equals to `foo` + // if each expression only references `foo`, declaration is needed + // to avoid reactivity tracking + // e.g., [[foo],[foo]] + if (vars.every(v => v.length === 1)) { + return true + } + + // if `foo` appears multiple times in one array, declaration is needed + // e.g., [[foo,foo]] + if (vars.some(v => v.filter(e => e === name).length > 1)) { + return true + } + + const first = vars[0] + // if arrays have different lengths, declaration is needed + // e.g., [[foo],[foo,bar]] + if (vars.some(v => v.length !== first.length)) { + // special case, no declaration needed if one array is a subset of the other + // because they will be treated as repeated expressions + // e.g., [[foo,bar],[foo,foo,bar]] -> const foo_bar = _ctx.foo + _ctx.bar + if ( + vars.some( + v => v.length > first.length && v.every(e => first.includes(e)), + ) || + vars.some(v => first.length > v.length && first.every(e => v.includes(e))) + ) { + return false + } + return true + } + // if arrays share common elements, no declaration needed + // because they will be treat as repeated expressions + // e.g., [[foo,bar],[foo,bar]] -> const foo_bar = _ctx.foo + _ctx.bar + if (vars.some(v => v.some(e => first.includes(e)))) { + return false + } + + return true +} + +function processRepeatedExpressions( + context: CodegenContext, + expressions: SimpleExpressionNode[], + varDeclarations: DeclarationValue[], +): DeclarationValue[] { + const declarations: DeclarationValue[] = [] + const seenExp = expressions.reduce( + (acc, exp) => { + // only handle expressions that are not identifiers + if (exp.ast && exp.ast.type !== 'Identifier') { + acc[exp.content] = (acc[exp.content] || 0) + 1 + } + return acc + }, + Object.create(null) as Record, + ) + + Object.entries(seenExp).forEach(([content, count]) => { + if (count > 1) { + // foo + baz -> foo_baz + const varName = genVarName(content) + if (!declarations.some(d => d.name === varName)) { + // if foo and baz have no other references, we don't need to declare separate variables + // instead of: + // const foo = _ctx.foo + // const baz = _ctx.baz + // const foo_baz = foo + baz + // we can generate: + // const foo_baz = _ctx.foo + _ctx.baz + const delVars: Record = {} + for (let i = varDeclarations.length - 1; i >= 0; i--) { + const item = varDeclarations[i] + if (!item.exps || !item.seenCount) continue + + const shouldRemove = [...item.exps].every( + node => node.content === content && item.seenCount === count, + ) + if (shouldRemove) { + delVars[item.name] = item.rawName! + varDeclarations.splice(i, 1) + } + } + const value = extend( + {}, + expressions.find(exp => exp.content === content)!, + ) + Object.keys(delVars).forEach(name => { + value.content = value.content.replace(name, delVars[name]) + if (value.ast) value.ast = parseExp(context, value.content) + }) + declarations.push({ + name: varName, + value: value, + }) + } + + // assume content equals to `foo + baz` + expressions.forEach(exp => { + // foo + baz -> foo_baz + if (exp.content === content) { + exp.content = varName + // ast is no longer needed since it becomes an identifier. + exp.ast = null + } + // foo + foo + baz -> foo + foo_baz + else if (exp.content.includes(content)) { + exp.content = exp.content.replace( + new RegExp(escapeRegExp(content), 'g'), + varName, + ) + // re-parse the expression + exp.ast = parseExp(context, exp.content) + } + }) + } + }) + + return declarations +} + +function genDeclarations( + declarations: DeclarationValue[], + context: CodegenContext, +): DeclarationResult { + const [frag, push] = buildCodeFragment() + const ids: Record = Object.create(null) + + // process identifiers first as expressions may rely on them + declarations.forEach(({ name, isIdentifier, value }) => { + if (isIdentifier) { + const varName = (ids[name] = `_${name}`) + push(`const ${varName} = `, ...genExpression(value, context), NEWLINE) + } + }) + + // process expressions + declarations.forEach(({ name, isIdentifier, value }) => { + if (!isIdentifier) { + const varName = (ids[name] = `_${name}`) + push( + `const ${varName} = `, + ...context.withId(() => genExpression(value, context), ids), + NEWLINE, + ) + } + }) + + return { ids, frag } +} + +function escapeRegExp(string: string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +function parseExp(context: CodegenContext, content: string): Node { + const plugins = context.options.expressionPlugins + const options: ParserOptions = { + plugins: plugins ? [...plugins, 'typescript'] : ['typescript'], + } + return parseExpression(`(${content})`, options) +} + +function genVarName(exp: string): string { + return `${exp + .replace(/[^a-zA-Z0-9]/g, '_') + .replace(/_+/g, '_') + .replace(/_+$/, '')}` +} + +function extractMemberExpression( + exp: Node, + onIdentifier: (name: string) => void, +): string { + if (!exp) return '' + switch (exp.type) { + case 'Identifier': // foo[bar] + onIdentifier(exp.name) + return exp.name + case 'StringLiteral': // foo['bar'] + return exp.extra ? (exp.extra.raw as string) : exp.value + case 'NumericLiteral': // foo[0] + return exp.value.toString() + case 'BinaryExpression': // foo[bar + 1] + return `${extractMemberExpression(exp.left, onIdentifier)} ${exp.operator} ${extractMemberExpression(exp.right, onIdentifier)}` + case 'CallExpression': // foo[bar(baz)] + return `${extractMemberExpression(exp.callee, onIdentifier)}(${exp.arguments.map(arg => extractMemberExpression(arg, onIdentifier)).join(', ')})` + case 'MemberExpression': // foo[bar.baz] + const object = extractMemberExpression(exp.object, onIdentifier) + const prop = exp.computed + ? `[${extractMemberExpression(exp.property, onIdentifier)}]` + : `.${extractMemberExpression(exp.property, NOOP)}` + return `${object}${prop}` + default: + return '' + } +} diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts index 50a307d679..d8a57edea9 100644 --- a/packages/compiler-vapor/src/generators/operation.ts +++ b/packages/compiler-vapor/src/generators/operation.ts @@ -18,6 +18,7 @@ import { } from './utils' import { genCreateComponent } from './component' import { genSlotOutlet } from './slotOutlet' +import { processExpressions } from './expression' export function genOperations( opers: OperationNode[], @@ -81,13 +82,21 @@ export function genEffects( effects: IREffect[], context: CodegenContext, ): CodeFragment[] { - const { helper } = context + const { + helper, + block: { expressions }, + } = context const [frag, push, unshift] = buildCodeFragment() let operationsCount = 0 + const { ids, frag: declarationFrags } = processExpressions( + context, + expressions, + ) + push(...declarationFrags) for (let i = 0; i < effects.length; i++) { const effect = effects[i] operationsCount += effect.operations.length - const frags = genEffect(effect, context) + const frags = context.withId(() => genEffect(effect, context), ids) i > 0 && push(NEWLINE) if (frag[frag.length - 1] === ')' && frags[0] === '(') { push(';') @@ -96,7 +105,7 @@ export function genEffects( } const newLineCount = frag.filter(frag => frag === NEWLINE).length - if (newLineCount > 1 || operationsCount > 1) { + if (newLineCount > 1 || operationsCount > 1 || declarationFrags.length > 0) { unshift(`{`, INDENT_START, NEWLINE) push(INDENT_END, NEWLINE, '}') } diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index de119a4cc9..c2f5a115da 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -52,6 +52,7 @@ export interface BlockIRNode extends BaseIRNode { dynamic: IRDynamicInfo effect: IREffect[] operation: OperationNode[] + expressions: SimpleExpressionNode[] returns: number[] } diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 431054a6e4..bb4deb27cd 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -148,6 +148,8 @@ export class TransformContext { ) { return this.registerOperation(...operations) } + + this.block.expressions.push(...expressions) const existing = this.block.effect.find(e => isSameExpression(e.expressions, expressions), ) diff --git a/packages/compiler-vapor/src/transforms/utils.ts b/packages/compiler-vapor/src/transforms/utils.ts index 2a1248b4c1..9b7fb12728 100644 --- a/packages/compiler-vapor/src/transforms/utils.ts +++ b/packages/compiler-vapor/src/transforms/utils.ts @@ -29,6 +29,7 @@ export const newBlock = (node: BlockIRNode['node']): BlockIRNode => ({ effect: [], operation: [], returns: [], + expressions: [], }) export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {