fix(compiler-vapor): correct execution order of operations (#13351)

This commit is contained in:
zhiyuanzmj 2025-06-20 08:55:50 +08:00 committed by GitHub
parent 430216a9f5
commit bf7424aa29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 121 additions and 65 deletions

View File

@ -149,7 +149,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
`; `;
exports[`compile > directives > v-pre > should not affect siblings after it 1`] = ` exports[`compile > directives > v-pre > should not affect siblings after it 1`] = `
"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, child as _child, toDisplayString as _toDisplayString, setText as _setText, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; "import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>") const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>")
const t1 = _template("<div> </div>") const t1 = _template("<div> </div>")
@ -161,8 +161,8 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
_setInsertionState(n3, 0) _setInsertionState(n3, 0)
const n1 = _createComponentWithFallback(_component_Comp) const n1 = _createComponentWithFallback(_component_Comp)
_renderEffect(() => { _renderEffect(() => {
_setText(n2, _toDisplayString(_ctx.bar))
_setProp(n3, "id", _ctx.foo) _setProp(n3, "id", _ctx.foo)
_setText(n2, _toDisplayString(_ctx.bar))
}) })
return [n0, n3] return [n0, n3]
}" }"
@ -180,7 +180,7 @@ export function render(_ctx) {
`; `;
exports[`compile > dynamic root nodes and interpolation 1`] = ` exports[`compile > dynamic root nodes and interpolation 1`] = `
"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, setProp as _setProp, renderEffect as _renderEffect, delegateEvents as _delegateEvents, template as _template } from 'vue'; "import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, delegateEvents as _delegateEvents, template as _template } from 'vue';
const t0 = _template("<button> </button>", true) const t0 = _template("<button> </button>", true)
_delegateEvents("click") _delegateEvents("click")
@ -190,13 +190,47 @@ export function render(_ctx) {
n0.$evtclick = e => _ctx.handleClick(e) n0.$evtclick = e => _ctx.handleClick(e)
_renderEffect(() => { _renderEffect(() => {
const _count = _ctx.count const _count = _ctx.count
_setText(x0, _toDisplayString(_count) + "foo" + _toDisplayString(_count) + "foo" + _toDisplayString(_count))
_setProp(n0, "id", _count) _setProp(n0, "id", _count)
_setText(x0, _toDisplayString(_count) + "foo" + _toDisplayString(_count) + "foo" + _toDisplayString(_count))
}) })
return n0 return n0
}" }"
`; `;
exports[`compile > execution order > basic 1`] = `
"import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div> </div>", true)
export function render(_ctx) {
const n0 = t0()
const x0 = _child(n0)
_renderEffect(() => {
_setProp(n0, "id", _ctx.foo)
_setText(x0, _toDisplayString(_ctx.bar))
})
return n0
}"
`;
exports[`compile > execution order > with v-once 1`] = `
"import { child as _child, next as _next, nthChild as _nthChild, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div><span> </span> <br> </div>", true)
export function render(_ctx) {
const n3 = t0()
const n0 = _child(n3)
const n1 = _next(n0)
const n2 = _nthChild(n3, 3)
const x0 = _child(n0)
_setText(x0, _toDisplayString(_ctx.foo))
_renderEffect(() => {
_setText(n1, " " + _toDisplayString(_ctx.bar))
_setText(n2, " " + _toDisplayString(_ctx.baz))
})
return n3
}"
`;
exports[`compile > expression parsing > interpolation 1`] = ` exports[`compile > expression parsing > interpolation 1`] = `
" "
const n0 = t0() const n0 = t0()

View File

@ -237,4 +237,29 @@ describe('compile', () => {
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
}) })
describe('execution order', () => {
test('basic', () => {
const code = compile(`<div :id="foo">{{ bar }}</div>`)
expect(code).matchSnapshot()
expect(code).contains(
`_setProp(n0, "id", _ctx.foo)
_setText(x0, _toDisplayString(_ctx.bar))`,
)
})
test('with v-once', () => {
const code = compile(
`<div>
<span v-once>{{ foo }}</span>
{{ bar }}<br>
{{ baz }}
</div>`,
)
expect(code).matchSnapshot()
expect(code).contains(
`_setText(n1, " " + _toDisplayString(_ctx.bar))
_setText(n2, " " + _toDisplayString(_ctx.baz))`,
)
})
})
}) })

View File

@ -67,7 +67,6 @@ export function render(_ctx) {
const x2 = _child(n2) const x2 = _child(n2)
_renderEffect(() => { _renderEffect(() => {
const _msg = _ctx.msg const _msg = _ctx.msg
_setText(x0, _toDisplayString(_msg)) _setText(x0, _toDisplayString(_msg))
_setText(x1, _toDisplayString(_msg)) _setText(x1, _toDisplayString(_msg))
_setText(x2, _toDisplayString(_msg)) _setText(x2, _toDisplayString(_msg))

View File

@ -55,7 +55,6 @@ export function render(_ctx) {
const _foo = _ctx.foo const _foo = _ctx.foo
const _bar = _ctx.bar const _bar = _ctx.bar
const _foo_bar_baz = _foo[_bar(_ctx.baz)] const _foo_bar_baz = _foo[_bar(_ctx.baz)]
_setProp(n0, "id", _foo_bar_baz) _setProp(n0, "id", _foo_bar_baz)
_setProp(n1, "id", _foo_bar_baz) _setProp(n1, "id", _foo_bar_baz)
_setProp(n2, "id", _bar() + _foo) _setProp(n2, "id", _bar() + _foo)
@ -107,7 +106,6 @@ export function render(_ctx) {
_renderEffect(() => { _renderEffect(() => {
const _obj = _ctx.obj const _obj = _ctx.obj
const _obj_foo_baz_obj_bar = _obj['foo']['baz'] + _obj.bar const _obj_foo_baz_obj_bar = _obj['foo']['baz'] + _obj.bar
_setProp(n0, "id", _obj_foo_baz_obj_bar) _setProp(n0, "id", _obj_foo_baz_obj_bar)
_setProp(n1, "id", _obj_foo_baz_obj_bar) _setProp(n1, "id", _obj_foo_baz_obj_bar)
}) })
@ -126,7 +124,6 @@ export function render(_ctx) {
_renderEffect(() => { _renderEffect(() => {
const _foo = _ctx.foo const _foo = _ctx.foo
const _foo_bar = _foo + _ctx.bar const _foo_bar = _foo + _ctx.bar
_setProp(n0, "id", _foo_bar) _setProp(n0, "id", _foo_bar)
_setProp(n1, "id", _foo_bar) _setProp(n1, "id", _foo_bar)
_setProp(n2, "id", _foo + _foo_bar) _setProp(n2, "id", _foo + _foo_bar)
@ -144,7 +141,6 @@ export function render(_ctx) {
const n1 = t0() const n1 = t0()
_renderEffect(() => { _renderEffect(() => {
const _foo_bar = _ctx.foo + _ctx.bar const _foo_bar = _ctx.foo + _ctx.bar
_setProp(n0, "id", _foo_bar) _setProp(n0, "id", _foo_bar)
_setProp(n1, "id", _foo_bar) _setProp(n1, "id", _foo_bar)
}) })
@ -177,7 +173,6 @@ export function render(_ctx) {
const n1 = t0() const n1 = t0()
_renderEffect(() => { _renderEffect(() => {
const _foo = _ctx.foo const _foo = _ctx.foo
_setClass(n0, _foo) _setClass(n0, _foo)
_setClass(n1, _foo) _setClass(n1, _foo)
}) })
@ -498,15 +493,13 @@ export function render(_ctx) {
_setAttr(n0, "form", _ctx.form) _setAttr(n0, "form", _ctx.form)
_setAttr(n1, "list", _ctx.list) _setAttr(n1, "list", _ctx.list)
_setAttr(n2, "type", _ctx.type) _setAttr(n2, "type", _ctx.type)
_setAttr(n3, "width", _width) _setAttr(n3, "width", _width)
_setAttr(n4, "width", _width)
_setAttr(n5, "width", _width)
_setAttr(n6, "width", _width)
_setAttr(n3, "height", _height) _setAttr(n3, "height", _height)
_setAttr(n4, "width", _width)
_setAttr(n4, "height", _height) _setAttr(n4, "height", _height)
_setAttr(n5, "width", _width)
_setAttr(n5, "height", _height) _setAttr(n5, "height", _height)
_setAttr(n6, "width", _width)
_setAttr(n6, "height", _height) _setAttr(n6, "height", _height)
}) })
return [n0, n1, n2, n3, n4, n5, n6] return [n0, n1, n2, n3, n4, n5, n6]

View File

@ -81,8 +81,8 @@ export function getBaseTransformPreset(): TransformPreset {
transformVFor, transformVFor,
transformSlotOutlet, transformSlotOutlet,
transformTemplateRef, transformTemplateRef,
transformText,
transformElement, transformElement,
transformText,
transformVSlot, transformVSlot,
transformComment, transformComment,
transformChildren, transformChildren,

View File

@ -99,10 +99,8 @@ export function genEffects(
effects: IREffect[], effects: IREffect[],
context: CodegenContext, context: CodegenContext,
): CodeFragment[] { ): CodeFragment[] {
const { const { helper } = context
helper, const expressions = effects.flatMap(effect => effect.expressions)
block: { expressions },
} = context
const [frag, push, unshift] = buildCodeFragment() const [frag, push, unshift] = buildCodeFragment()
let operationsCount = 0 let operationsCount = 0
const { ids, frag: declarationFrags } = processExpressions( const { ids, frag: declarationFrags } = processExpressions(

View File

@ -52,7 +52,6 @@ export interface BlockIRNode extends BaseIRNode {
tempId: number tempId: number
effect: IREffect[] effect: IREffect[]
operation: OperationNode[] operation: OperationNode[]
expressions: SimpleExpressionNode[]
returns: number[] returns: number[]
} }

View File

@ -140,8 +140,10 @@ export class TransformContext<T extends AllNode = AllNode> {
registerEffect( registerEffect(
expressions: SimpleExpressionNode[], expressions: SimpleExpressionNode[],
...operations: OperationNode[] operation: OperationNode | OperationNode[],
getIndex = (): number => this.block.effect.length,
): void { ): void {
const operations = [operation].flat()
expressions = expressions.filter(exp => !isConstantExpression(exp)) expressions = expressions.filter(exp => !isConstantExpression(exp))
if ( if (
this.inVOnce || this.inVOnce ||
@ -153,26 +155,10 @@ export class TransformContext<T extends AllNode = AllNode> {
return this.registerOperation(...operations) return this.registerOperation(...operations)
} }
this.block.expressions.push(...expressions) this.block.effect.splice(getIndex(), 0, {
const existing = this.block.effect.find(e => expressions,
isSameExpression(e.expressions, expressions), operations,
) })
if (existing) {
existing.operations.push(...operations)
} else {
this.block.effect.push({
expressions,
operations,
})
}
function isSameExpression(
a: SimpleExpressionNode[],
b: SimpleExpressionNode[],
) {
if (a.length !== b.length) return false
return a.every((exp, i) => exp.content === b[i].content)
}
} }
registerOperation(...node: OperationNode[]): void { registerOperation(...node: OperationNode[]): void {

View File

@ -44,6 +44,8 @@ export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap(
) )
export const transformElement: NodeTransform = (node, context) => { export const transformElement: NodeTransform = (node, context) => {
let effectIndex = context.block.effect.length
const getEffectIndex = () => effectIndex++
return function postTransformElement() { return function postTransformElement() {
;({ node } = context) ;({ node } = context)
if ( if (
@ -62,6 +64,7 @@ export const transformElement: NodeTransform = (node, context) => {
context as TransformContext<ElementNode>, context as TransformContext<ElementNode>,
isComponent, isComponent,
isDynamicComponent, isDynamicComponent,
getEffectIndex,
) )
let { parent } = context let { parent } = context
@ -78,13 +81,23 @@ export const transformElement: NodeTransform = (node, context) => {
parent.node.children.filter(child => child.type !== NodeTypes.COMMENT) parent.node.children.filter(child => child.type !== NodeTypes.COMMENT)
.length === 1 .length === 1
;(isComponent ? transformComponentElement : transformNativeElement)( if (isComponent) {
node as any, transformComponentElement(
propsResult, node as ComponentNode,
singleRoot, propsResult,
context as TransformContext<ElementNode>, singleRoot,
isDynamicComponent, context,
) isDynamicComponent,
)
} else {
transformNativeElement(
node as PlainElementNode,
propsResult,
singleRoot,
context,
getEffectIndex,
)
}
} }
} }
@ -183,7 +196,8 @@ function transformNativeElement(
node: PlainElementNode, node: PlainElementNode,
propsResult: PropsResult, propsResult: PropsResult,
singleRoot: boolean, singleRoot: boolean,
context: TransformContext<ElementNode>, context: TransformContext,
getEffectIndex: () => number,
) { ) {
const { tag } = node const { tag } = node
const { scopeId } = context.options const { scopeId } = context.options
@ -196,12 +210,16 @@ function transformNativeElement(
const dynamicProps: string[] = [] const dynamicProps: string[] = []
if (propsResult[0] /* dynamic props */) { if (propsResult[0] /* dynamic props */) {
const [, dynamicArgs, expressions] = propsResult const [, dynamicArgs, expressions] = propsResult
context.registerEffect(expressions, { context.registerEffect(
type: IRNodeTypes.SET_DYNAMIC_PROPS, expressions,
element: context.reference(), {
props: dynamicArgs, type: IRNodeTypes.SET_DYNAMIC_PROPS,
root: singleRoot, element: context.reference(),
}) props: dynamicArgs,
root: singleRoot,
},
getEffectIndex,
)
} else { } else {
for (const prop of propsResult[1]) { for (const prop of propsResult[1]) {
const { key, values } = prop const { key, values } = prop
@ -210,13 +228,17 @@ function transformNativeElement(
if (values[0].content) template += `="${values[0].content}"` if (values[0].content) template += `="${values[0].content}"`
} else { } else {
dynamicProps.push(key.content) dynamicProps.push(key.content)
context.registerEffect(values, { context.registerEffect(
type: IRNodeTypes.SET_PROP, values,
element: context.reference(), {
prop, type: IRNodeTypes.SET_PROP,
root: singleRoot, element: context.reference(),
tag, prop,
}) root: singleRoot,
tag,
},
getEffectIndex,
)
} }
} }
} }
@ -253,6 +275,7 @@ export function buildProps(
context: TransformContext<ElementNode>, context: TransformContext<ElementNode>,
isComponent: boolean, isComponent: boolean,
isDynamicComponent?: boolean, isDynamicComponent?: boolean,
getEffectIndex?: () => number,
): PropsResult { ): PropsResult {
const props = node.props as (VaporDirectiveNode | AttributeNode)[] const props = node.props as (VaporDirectiveNode | AttributeNode)[]
if (props.length === 0) return [false, []] if (props.length === 0) return [false, []]
@ -299,12 +322,12 @@ export function buildProps(
} else { } else {
context.registerEffect( context.registerEffect(
[prop.exp], [prop.exp],
{ {
type: IRNodeTypes.SET_DYNAMIC_EVENTS, type: IRNodeTypes.SET_DYNAMIC_EVENTS,
element: context.reference(), element: context.reference(),
event: prop.exp, event: prop.exp,
}, },
getEffectIndex,
) )
} }
} else { } else {

View File

@ -29,7 +29,6 @@ export const newBlock = (node: BlockIRNode['node']): BlockIRNode => ({
effect: [], effect: [],
operation: [], operation: [],
returns: [], returns: [],
expressions: [],
tempId: 0, tempId: 0,
}) })