refactor(compiler-vapor): cache multiple access to the same expression (#12568)
This commit is contained in:
parent
757b3df56e
commit
58b49749c7
|
@ -182,8 +182,9 @@ export function render(_ctx) {
|
||||||
const n0 = t0()
|
const n0 = t0()
|
||||||
_delegate(n0, "click", () => _ctx.handleClick)
|
_delegate(n0, "click", () => _ctx.handleClick)
|
||||||
_renderEffect(() => {
|
_renderEffect(() => {
|
||||||
_setText(n0, _ctx.count, "foo", _ctx.count, "foo", _ctx.count)
|
const _count = _ctx.count
|
||||||
_setProp(n0, "id", _ctx.count)
|
_setText(n0, _count, "foo", _count, "foo", _count)
|
||||||
|
_setProp(n0, "id", _count)
|
||||||
})
|
})
|
||||||
return n0
|
return n0
|
||||||
}"
|
}"
|
||||||
|
@ -199,7 +200,10 @@ exports[`compile > expression parsing > interpolation 1`] = `
|
||||||
exports[`compile > expression parsing > v-bind 1`] = `
|
exports[`compile > expression parsing > v-bind 1`] = `
|
||||||
"
|
"
|
||||||
const n0 = t0()
|
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
|
return n0
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -193,9 +193,10 @@ describe('compile', () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(code).matchSnapshot()
|
expect(code).matchSnapshot()
|
||||||
expect(code).contains('key.value+1')
|
expect(code).contains('const _key = key.value')
|
||||||
|
expect(code).contains('_key+1')
|
||||||
expect(code).contains(
|
expect(code).contains(
|
||||||
'_setDynamicProps(n0, [{ [key.value+1]: _unref(foo)[key.value+1]() }], true)',
|
'_setDynamicProps(n0, [{ [_key+1]: _unref(foo)[_key+1]() }], true)',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,153 @@
|
||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// 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`] = `
|
exports[`compiler v-bind > .attr modifier 1`] = `
|
||||||
"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
|
"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
|
||||||
const t0 = _template("<div></div>", true)
|
const t0 = _template("<div></div>", true)
|
||||||
|
@ -305,6 +453,8 @@ export function render(_ctx) {
|
||||||
const n5 = t5()
|
const n5 = t5()
|
||||||
const n6 = t6()
|
const n6 = t6()
|
||||||
_renderEffect(() => {
|
_renderEffect(() => {
|
||||||
|
const _width = _ctx.width
|
||||||
|
const _height = _ctx.height
|
||||||
_setAttr(n0, "spellcheck", _ctx.spellcheck)
|
_setAttr(n0, "spellcheck", _ctx.spellcheck)
|
||||||
_setAttr(n0, "draggable", _ctx.draggable)
|
_setAttr(n0, "draggable", _ctx.draggable)
|
||||||
_setAttr(n0, "translate", _ctx.translate)
|
_setAttr(n0, "translate", _ctx.translate)
|
||||||
|
@ -312,15 +462,15 @@ export function render(_ctx) {
|
||||||
_setAttr(n1, "list", _ctx.list)
|
_setAttr(n1, "list", _ctx.list)
|
||||||
_setAttr(n2, "type", _ctx.type)
|
_setAttr(n2, "type", _ctx.type)
|
||||||
|
|
||||||
_setAttr(n3, "width", _ctx.width)
|
_setAttr(n3, "width", _width)
|
||||||
_setAttr(n4, "width", _ctx.width)
|
_setAttr(n4, "width", _width)
|
||||||
_setAttr(n5, "width", _ctx.width)
|
_setAttr(n5, "width", _width)
|
||||||
_setAttr(n6, "width", _ctx.width)
|
_setAttr(n6, "width", _width)
|
||||||
|
|
||||||
_setAttr(n3, "height", _ctx.height)
|
_setAttr(n3, "height", _height)
|
||||||
_setAttr(n4, "height", _ctx.height)
|
_setAttr(n4, "height", _height)
|
||||||
_setAttr(n5, "height", _ctx.height)
|
_setAttr(n5, "height", _height)
|
||||||
_setAttr(n6, "height", _ctx.height)
|
_setAttr(n6, "height", _height)
|
||||||
})
|
})
|
||||||
return [n0, n1, n2, n3, n4, n5, n6]
|
return [n0, n1, n2, n3, n4, n5, n6]
|
||||||
}"
|
}"
|
||||||
|
@ -343,7 +493,11 @@ const t0 = _template("<div></div>", true)
|
||||||
|
|
||||||
export function render(_ctx) {
|
export function render(_ctx) {
|
||||||
const n0 = t0()
|
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
|
return n0
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
@ -354,7 +508,10 @@ const t0 = _template("<div></div>", true)
|
||||||
|
|
||||||
export function render(_ctx) {
|
export function render(_ctx) {
|
||||||
const n0 = t0()
|
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
|
return n0
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -171,7 +171,7 @@ describe('compiler v-bind', () => {
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
expect(code).contains(
|
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(
|
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(n0, "form", _ctx.form)')
|
||||||
expect(code).contains('_setAttr(n1, "list", _ctx.list)')
|
expect(code).contains('_setAttr(n1, "list", _ctx.list)')
|
||||||
expect(code).contains('_setAttr(n2, "type", _ctx.type)')
|
expect(code).contains('_setAttr(n2, "type", _ctx.type)')
|
||||||
expect(code).contains('_setAttr(n3, "width", _ctx.width)')
|
expect(code).contains('_setAttr(n3, "width", _width)')
|
||||||
expect(code).contains('_setAttr(n3, "height", _ctx.height)')
|
expect(code).contains('_setAttr(n3, "height", _height)')
|
||||||
expect(code).contains('_setAttr(n4, "width", _ctx.width)')
|
expect(code).contains('_setAttr(n4, "width", _width)')
|
||||||
expect(code).contains('_setAttr(n4, "height", _ctx.height)')
|
expect(code).contains('_setAttr(n4, "height", _height)')
|
||||||
expect(code).contains('_setAttr(n5, "width", _ctx.width)')
|
expect(code).contains('_setAttr(n5, "width", _width)')
|
||||||
expect(code).contains('_setAttr(n5, "height", _ctx.height)')
|
expect(code).contains('_setAttr(n5, "height", _height)')
|
||||||
expect(code).contains(' _setAttr(n6, "width", _ctx.width)')
|
expect(code).contains(' _setAttr(n6, "width", _width)')
|
||||||
})
|
})
|
||||||
|
|
||||||
test(':innerHTML', () => {
|
test(':innerHTML', () => {
|
||||||
|
@ -694,3 +694,102 @@ describe('compiler v-bind', () => {
|
||||||
expect(code).matchSnapshot()
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import { genPropsAccessExp, isGloballyAllowed } from '@vue/shared'
|
import { NOOP, extend, genPropsAccessExp, isGloballyAllowed } from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
BindingTypes,
|
BindingTypes,
|
||||||
NewlineType,
|
NewlineType,
|
||||||
type SimpleExpressionNode,
|
type SimpleExpressionNode,
|
||||||
type SourceLocation,
|
type SourceLocation,
|
||||||
advancePositionWithClone,
|
advancePositionWithClone,
|
||||||
|
createSimpleExpression,
|
||||||
isInDestructureAssignment,
|
isInDestructureAssignment,
|
||||||
isStaticProperty,
|
isStaticProperty,
|
||||||
walkIdentifiers,
|
walkIdentifiers,
|
||||||
} from '@vue/compiler-dom'
|
} from '@vue/compiler-dom'
|
||||||
import type { Identifier } from '@babel/types'
|
import type { Identifier, Node } from '@babel/types'
|
||||||
import type { CodegenContext } from '../generate'
|
import type { CodegenContext } from '../generate'
|
||||||
import type { Node } from '@babel/types'
|
|
||||||
import { isConstantExpression } from '../utils'
|
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(
|
export function genExpression(
|
||||||
node: SimpleExpressionNode,
|
node: SimpleExpressionNode,
|
||||||
|
@ -209,3 +211,347 @@ function canPrefix(name: string) {
|
||||||
return false
|
return false
|
||||||
return true
|
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 ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { genCreateComponent } from './component'
|
import { genCreateComponent } from './component'
|
||||||
import { genSlotOutlet } from './slotOutlet'
|
import { genSlotOutlet } from './slotOutlet'
|
||||||
|
import { processExpressions } from './expression'
|
||||||
|
|
||||||
export function genOperations(
|
export function genOperations(
|
||||||
opers: OperationNode[],
|
opers: OperationNode[],
|
||||||
|
@ -81,13 +82,21 @@ export function genEffects(
|
||||||
effects: IREffect[],
|
effects: IREffect[],
|
||||||
context: CodegenContext,
|
context: CodegenContext,
|
||||||
): CodeFragment[] {
|
): CodeFragment[] {
|
||||||
const { helper } = context
|
const {
|
||||||
|
helper,
|
||||||
|
block: { expressions },
|
||||||
|
} = context
|
||||||
const [frag, push, unshift] = buildCodeFragment()
|
const [frag, push, unshift] = buildCodeFragment()
|
||||||
let operationsCount = 0
|
let operationsCount = 0
|
||||||
|
const { ids, frag: declarationFrags } = processExpressions(
|
||||||
|
context,
|
||||||
|
expressions,
|
||||||
|
)
|
||||||
|
push(...declarationFrags)
|
||||||
for (let i = 0; i < effects.length; i++) {
|
for (let i = 0; i < effects.length; i++) {
|
||||||
const effect = effects[i]
|
const effect = effects[i]
|
||||||
operationsCount += effect.operations.length
|
operationsCount += effect.operations.length
|
||||||
const frags = genEffect(effect, context)
|
const frags = context.withId(() => genEffect(effect, context), ids)
|
||||||
i > 0 && push(NEWLINE)
|
i > 0 && push(NEWLINE)
|
||||||
if (frag[frag.length - 1] === ')' && frags[0] === '(') {
|
if (frag[frag.length - 1] === ')' && frags[0] === '(') {
|
||||||
push(';')
|
push(';')
|
||||||
|
@ -96,7 +105,7 @@ export function genEffects(
|
||||||
}
|
}
|
||||||
|
|
||||||
const newLineCount = frag.filter(frag => frag === NEWLINE).length
|
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)
|
unshift(`{`, INDENT_START, NEWLINE)
|
||||||
push(INDENT_END, NEWLINE, '}')
|
push(INDENT_END, NEWLINE, '}')
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@ export interface BlockIRNode extends BaseIRNode {
|
||||||
dynamic: IRDynamicInfo
|
dynamic: IRDynamicInfo
|
||||||
effect: IREffect[]
|
effect: IREffect[]
|
||||||
operation: OperationNode[]
|
operation: OperationNode[]
|
||||||
|
expressions: SimpleExpressionNode[]
|
||||||
returns: number[]
|
returns: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -148,6 +148,8 @@ export class TransformContext<T extends AllNode = AllNode> {
|
||||||
) {
|
) {
|
||||||
return this.registerOperation(...operations)
|
return this.registerOperation(...operations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.block.expressions.push(...expressions)
|
||||||
const existing = this.block.effect.find(e =>
|
const existing = this.block.effect.find(e =>
|
||||||
isSameExpression(e.expressions, expressions),
|
isSameExpression(e.expressions, expressions),
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,6 +29,7 @@ export const newBlock = (node: BlockIRNode['node']): BlockIRNode => ({
|
||||||
effect: [],
|
effect: [],
|
||||||
operation: [],
|
operation: [],
|
||||||
returns: [],
|
returns: [],
|
||||||
|
expressions: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {
|
export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {
|
||||||
|
|
Loading…
Reference in New Issue