diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap index ea11cf71..48c22aef 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap @@ -8,7 +8,7 @@ export default function render() { return (openBlock(), createBlock(\\"input\\", { modelValue: _ctx.model[_ctx.index], \\"onUpdate:modelValue\\": $event => (_ctx.model[_ctx.index] = $event) - }, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"])) + }, null, 8 /* PROPS */, [\\"modelValue\\"])) }" `; @@ -35,7 +35,7 @@ export default function render() { return (openBlock(), createBlock(\\"input\\", { modelValue: _ctx.model, \\"onUpdate:modelValue\\": $event => (_ctx.model = $event) - }, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"])) + }, null, 8 /* PROPS */, [\\"modelValue\\"])) }" `; diff --git a/packages/compiler-core/__tests__/transforms/vModel.spec.ts b/packages/compiler-core/__tests__/transforms/vModel.spec.ts index 3eed83cc..42f3d8c5 100644 --- a/packages/compiler-core/__tests__/transforms/vModel.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vModel.spec.ts @@ -5,18 +5,29 @@ import { ElementNode, ObjectExpression, CompilerOptions, - CallExpression + CallExpression, + ForNode, + PlainElementNode, + PlainElementCodegenNode, + ComponentNode } from '../../src' import { ErrorCodes } from '../../src/errors' import { transformModel } from '../../src/transforms/vModel' import { transformElement } from '../../src/transforms/transformElement' import { transformExpression } from '../../src/transforms/transformExpression' +import { transformFor } from '../../src/transforms/vFor' +import { trackSlotScopes } from '../../src/transforms/vSlot' function parseWithVModel(template: string, options: CompilerOptions = {}) { const ast = parse(template) transform(ast, { - nodeTransforms: [transformExpression, transformElement], + nodeTransforms: [ + transformFor, + transformExpression, + trackSlotScopes, + transformElement + ], directiveTransforms: { ...options.directiveTransforms, model: transformModel @@ -327,6 +338,39 @@ describe('compiler: transform v-model', () => { expect(generate(root, { mode: 'module' }).code).toMatchSnapshot() }) + test('should not mark update handler dynamic', () => { + const root = parseWithVModel('', { + prefixIdentifiers: true + }) + const codegen = (root.children[0] as PlainElementNode) + .codegenNode as PlainElementCodegenNode + expect(codegen.arguments[4]).toBe(`["modelValue"]`) + }) + + test('should mark update handler dynamic if it refers v-for scope variables', () => { + const root = parseWithVModel( + '', + { + prefixIdentifiers: true + } + ) + const codegen = ((root.children[0] as ForNode) + .children[0] as PlainElementNode).codegenNode as PlainElementCodegenNode + expect(codegen.arguments[4]).toBe(`["modelValue", "onUpdate:modelValue"]`) + }) + + test('should mark update handler dynamic if it refers slot scope variables', () => { + const root = parseWithVModel( + '', + { + prefixIdentifiers: true + } + ) + const codegen = ((root.children[0] as ComponentNode) + .children[0] as PlainElementNode).codegenNode as PlainElementCodegenNode + expect(codegen.arguments[4]).toBe(`["modelValue", "onUpdate:modelValue"]`) + }) + describe('errors', () => { test('missing expression', () => { const onError = jest.fn() diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index 571eb89d..183a1f98 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -193,21 +193,21 @@ export function buildProps( const analyzePatchFlag = ({ key, value }: Property) => { if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) { if ( - value.type !== NodeTypes.SIMPLE_EXPRESSION || - // E.g:

. - // Do not add prop `foo` to `dynamicPropNames`. - (!value.isStatic && !value.isConstant) + (value.type === NodeTypes.SIMPLE_EXPRESSION || + value.type === NodeTypes.COMPOUND_EXPRESSION) && + isStaticNode(value) ) { - const name = key.content - if (name === 'ref') { - hasRef = true - } else if (name === 'class') { - hasClassBinding = true - } else if (name === 'style') { - hasStyleBinding = true - } else if (name !== 'key') { - dynamicPropNames.push(name) - } + return + } + const name = key.content + if (name === 'ref') { + hasRef = true + } else if (name === 'class') { + hasClassBinding = true + } else if (name === 'style') { + hasStyleBinding = true + } else if (name !== 'key') { + dynamicPropNames.push(name) } } else { hasDynamicKeys = true diff --git a/packages/compiler-core/src/transforms/vModel.ts b/packages/compiler-core/src/transforms/vModel.ts index 86857ce3..00dcc84c 100644 --- a/packages/compiler-core/src/transforms/vModel.ts +++ b/packages/compiler-core/src/transforms/vModel.ts @@ -1,13 +1,16 @@ -import { DirectiveTransform } from '../transform' +import { DirectiveTransform, TransformContext } from '../transform' import { createSimpleExpression, createObjectProperty, createCompoundExpression, NodeTypes, - Property + Property, + CompoundExpressionNode, + createInterpolation } from '../ast' import { createCompilerError, ErrorCodes } from '../errors' import { isMemberExpression } from '../utils' +import { isObject } from '@vue/shared' export const transformModel: DirectiveTransform = (dir, node, context) => { const { exp, arg } = dir @@ -38,13 +41,23 @@ export const transformModel: DirectiveTransform = (dir, node, context) => { ]) : createSimpleExpression('onUpdate:modelValue', true) + let assignmentChildren = + exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children + // For a member expression used in assignment, it only needs to be updated + // if the expression involves scope variables. Otherwise we can mark the + // expression as constant to avoid it being included in `dynamicPropNames` + // of the element. This optimization relies on `prefixIdentifiers: true`. + if (!__BROWSER__ && context.prefixIdentifiers) { + assignmentChildren = assignmentChildren.map(c => toConstant(c, context)) + } + const props = [ createObjectProperty(propName, dir.exp!), createObjectProperty( eventName, createCompoundExpression([ `$event => (`, - ...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children), + ...assignmentChildren, ` = $event)` ]) ) @@ -57,6 +70,30 @@ export const transformModel: DirectiveTransform = (dir, node, context) => { return createTransformProps(props) } +function toConstant( + exp: CompoundExpressionNode | CompoundExpressionNode['children'][0], + context: TransformContext +): any { + if (!isObject(exp) || exp.type === NodeTypes.TEXT) { + return exp + } + if (exp.type === NodeTypes.SIMPLE_EXPRESSION) { + if (exp.isStatic || context.identifiers[exp.content]) { + return exp + } + return { + ...exp, + isConstant: true + } + } else if (exp.type === NodeTypes.COMPOUND_EXPRESSION) { + return createCompoundExpression( + exp.children.map(c => toConstant(c, context)) + ) + } else if (exp.type === NodeTypes.INTERPOLATION) { + return createInterpolation(toConstant(exp.content, context), exp.loc) + } +} + function createTransformProps(props: Property[] = []) { return { props, needRuntime: false } }