]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(compiler-vapor): cache multiple access to the same expression (#12568)
authoredison <daiwei521@126.com>
Wed, 8 Jan 2025 07:05:48 +0000 (15:05 +0800)
committerGitHub <noreply@github.com>
Wed, 8 Jan 2025 07:05:48 +0000 (15:05 +0800)
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
packages/compiler-vapor/__tests__/compile.spec.ts
packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
packages/compiler-vapor/src/generators/expression.ts
packages/compiler-vapor/src/generators/operation.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transform.ts
packages/compiler-vapor/src/transforms/utils.ts

index fad91a7952d9c62183ddbdcba5123b1cd8a21764..2d553fcea850a854a72aa9c26905a5d96c09f9b5 100644 (file)
@@ -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
 "
 `;
index 1699d350288930c6af35f544ea980fffbefd672f..33f399caa7705167b5902e4145b62f1b64d23a21 100644 (file)
@@ -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)',
       )
     })
 
index 857b3a5d00b44d92a90168d5533202414c5e460a..24585e39ea39dd949eaaf41aaa72eac69d369ef8 100644 (file)
@@ -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("<div></div>", 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("<div></div>", 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("<div></div>")
+
+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("<div></div>", 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("<div></div>")
+
+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("<div></div>")
+
+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("<div></div>")
+
+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("<div></div>")
+
+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("<div></div>")
+
+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("<div></div>", 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("<div></div>", 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("<div></div>", 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
 }"
 `;
index d785adfe810e439863f09d5d1abf21a18183707f..5025997e20a880006f88bd608472176f3af7fa19 100644 (file)
@@ -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(`
+        <div :class="foo"></div>
+        <div :class="foo"></div>
+      `)
+    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(`
+        <div :id="foo + bar"></div>
+        <div :id="foo + bar"></div>
+      `)
+    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(`
+        <div :id="foo + foo + bar"></div>
+        <div :id="foo"></div>
+      `)
+    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(`
+        <div :id="foo + bar"></div>
+        <div :id="foo + bar"></div>
+        <div :id="foo + foo + bar"></div>
+      `)
+    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(`
+        <div :id="foo[bar(baz)]"></div>
+        <div :id="foo[bar(baz)]"></div>
+        <div :id="bar() + foo"></div>
+      `)
+    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(`
+        <div :[key+1]="foo[key+1]()" />
+      `)
+    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(`
+        <div :id="obj['foo']['baz'] + obj.bar"></div>
+        <div :id="obj['foo']['baz'] + obj.bar"></div>
+      `)
+    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(`
+        <div :id="obj[1][baz] + obj.bar"></div>
+      `)
+    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(`
+        <div :id="bar + obj.bar"></div>
+      `)
+    expect(code).matchSnapshot()
+    expect(code).not.contains('const _bar = _ctx.bar')
+  })
+})
index 990659b84e405c9631e29832defc8abb0e21fa1e..8e71458337e7d950c9d02c49af1dff91877496df 100644 (file)
@@ -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<string, string>
+  frag: CodeFragment[]
+}
+type DeclarationValue = {
+  name: string
+  isIdentifier?: boolean
+  value: SimpleExpressionNode
+  rawName?: string
+  exps?: Set<SimpleExpressionNode>
+  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<string, number> = Object.create(null)
+  const variableToExpMap = new Map<string, Set<SimpleExpressionNode>>()
+  const expToVariableMap = new Map<SimpleExpressionNode, string[]>()
+  const seenIdentifier = new Set<string>()
+
+  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<string, number>,
+  variableToExpMap: Map<string, Set<SimpleExpressionNode>>,
+  expToVariableMap: Map<SimpleExpressionNode, string[]>,
+  seenIdentifier: Set<string>,
+): 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<SimpleExpressionNode, string[]>,
+  exps: Set<SimpleExpressionNode>,
+): 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<string, number>,
+  )
+
+  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<string, string> = {}
+        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<string, string> = 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 ''
+  }
+}
index 50a307d6798477a1dc2004990fb62c636d931594..d8a57edea926da89465273dc6cd748d85d7e0f59 100644 (file)
@@ -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, '}')
   }
index de119a4cc98225c5fa3d0538f919f9e4e85e4e24..c2f5a115da31642ffe25b98fc46e07c31eab973b 100644 (file)
@@ -52,6 +52,7 @@ export interface BlockIRNode extends BaseIRNode {
   dynamic: IRDynamicInfo
   effect: IREffect[]
   operation: OperationNode[]
+  expressions: SimpleExpressionNode[]
   returns: number[]
 }
 
index 431054a6e43438cb16442717ebb0a6c2e353a47a..bb4deb27cda32465c89758958cfc4fae23045e89 100644 (file)
@@ -148,6 +148,8 @@ export class TransformContext<T extends AllNode = AllNode> {
     ) {
       return this.registerOperation(...operations)
     }
+
+    this.block.expressions.push(...expressions)
     const existing = this.block.effect.find(e =>
       isSameExpression(e.expressions, expressions),
     )
index 2a1248b4c11215e3d9242cd9f923b38690132ee4..9b7fb127288f873129ed3a759b4613e0d32ccce0 100644 (file)
@@ -29,6 +29,7 @@ export const newBlock = (node: BlockIRNode['node']): BlockIRNode => ({
   effect: [],
   operation: [],
   returns: [],
+  expressions: [],
 })
 
 export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {