feat(compiler-core/v-model): avoid patching v-model handler when possible

This commit is contained in:
Evan You 2019-10-16 13:56:00 -04:00
parent 48b79d02e8
commit 5481f76ce8
4 changed files with 102 additions and 21 deletions

View File

@ -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\\"]))
}"
`;

View File

@ -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('<input v-model="foo" />', {
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(
'<input v-for="i in list" v-model="foo[i]" />',
{
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(
'<Comp v-slot="{ foo }"><input v-model="foo"/></Comp>',
{
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()

View File

@ -193,11 +193,12 @@ export function buildProps(
const analyzePatchFlag = ({ key, value }: Property) => {
if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
if (
value.type !== NodeTypes.SIMPLE_EXPRESSION ||
// E.g: <p :foo="1 + 2" />.
// Do not add prop `foo` to `dynamicPropNames`.
(!value.isStatic && !value.isConstant)
(value.type === NodeTypes.SIMPLE_EXPRESSION ||
value.type === NodeTypes.COMPOUND_EXPRESSION) &&
isStaticNode(value)
) {
return
}
const name = key.content
if (name === 'ref') {
hasRef = true
@ -208,7 +209,6 @@ export function buildProps(
} else if (name !== 'key') {
dynamicPropNames.push(name)
}
}
} else {
hasDynamicKeys = true
}

View File

@ -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 }
}